Commit abe808bc by Torkel Ödegaard Committed by GitHub

ToolbarButton: New emotion based component to replace all navbar, DashNavButton…

ToolbarButton: New emotion based component to replace all navbar, DashNavButton and scss styles (#30333)

* ToolbarButton: New emotion based component to replace all navbar, DashNavButton and scss styles

* Component ready for use

* Dam dam dam

* Starting big button design update

* Tried to use main button component but failed

* Minor fix

* Updates

* Updated

* Update packages/grafana-ui/src/components/Button/Button.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update packages/grafana-ui/src/components/Button/ButtonGroup.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Updated to use spacing base

* Button updates

* Removd unused import

* Remove unused import

* Use correct theme variable for border-radius

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
parent c0cddc30
......@@ -53,6 +53,7 @@ export interface GrafanaThemeCommons {
};
};
spacing: {
base: number;
insetSquishMd: string;
d: string;
xxs: string;
......
import React from 'react';
import { Story } from '@storybook/react';
import { Button, ButtonProps } from './Button';
import { Button, ButtonProps, ButtonVariant } from './Button';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { iconOptions } from '../../utils/storybook/knobs';
import mdx from './Button.mdx';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
import { ButtonGroup } from './ButtonGroup';
import { ComponentSize } from '../../types/size';
export default {
title: 'Buttons/Button',
......@@ -26,11 +29,53 @@ export default {
},
};
export const Simple: Story<ButtonProps> = ({ children, ...args }) => <Button {...args}>{children}</Button>;
Simple.args = {
variant: 'primary',
size: 'md',
disabled: false,
children: 'Button',
icon: undefined,
export const Variants: Story<ButtonProps> = ({ children, ...args }) => {
const sizes: ComponentSize[] = ['lg', 'md', 'sm'];
const variants: ButtonVariant[] = ['primary', 'secondary', 'destructive', 'link'];
return (
<VerticalGroup>
<HorizontalGroup spacing="lg">
{variants.map(variant => (
<VerticalGroup spacing="lg" key={variant}>
{sizes.map(size => (
<Button variant={variant} size={size} key={size}>
{variant} {size}
</Button>
))}
</VerticalGroup>
))}
</HorizontalGroup>
<div />
<HorizontalGroup spacing="lg">
<div>With icon and text</div>
<Button icon="cloud" size="sm">
Configure
</Button>
<Button icon="cloud">Configure</Button>
<Button icon="cloud" size="lg">
Configure
</Button>
</HorizontalGroup>
<div />
<HorizontalGroup spacing="lg">
<div>With icon only</div>
<Button icon="cloud" size="sm" />
<Button icon="cloud" size="md" />
<Button icon="cloud" size="lg" />
</HorizontalGroup>
<div />
<Button icon="plus" fullWidth>
Button with fullWidth
</Button>
<div />
<HorizontalGroup spacing="lg">
<div>Inside ButtonGroup</div>
<ButtonGroup noSpacing>
<Button icon="sync">Run query</Button>
<Button icon="angle-down" />
</ButtonGroup>
</HorizontalGroup>
</VerticalGroup>
);
};
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, useContext } from 'react';
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import { css, cx } from 'emotion';
import tinycolor from 'tinycolor2';
import { stylesFactory, ThemeContext } from '../../themes';
import { useTheme } from '../../themes';
import { IconName } from '../../types/icon';
import { getFocusStyle, getPropertiesForButtonSize } from '../Forms/commonStyles';
import { getPropertiesForButtonSize } from '../Forms/commonStyles';
import { GrafanaTheme } from '@grafana/data';
import { ButtonContent } from './ButtonContent';
import { ComponentSize } from '../../types/size';
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 getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
switch (variant) {
case 'secondary':
const from = theme.isLight ? theme.palette.gray7 : theme.palette.gray15;
const to = theme.isLight
? tinycolor(from)
.darken(5)
.toString()
: tinycolor(from)
.lighten(4)
.toString();
return {
borderColor: theme.isLight ? theme.palette.gray85 : theme.palette.gray25,
background: buttonVariantStyles(from, to, theme.isLight ? theme.palette.gray25 : theme.palette.gray4),
};
case 'destructive':
return {
borderColor: theme.palette.redShade,
background: buttonVariantStyles(theme.palette.redBase, theme.palette.redShade, theme.palette.white),
};
case 'link':
return {
borderColor: 'transparent',
background: buttonVariantStyles('transparent', 'transparent', theme.colors.linkExternal),
variantStyles: css`
&:focus {
outline: none;
}
`,
};
case 'primary':
default:
return {
borderColor: theme.colors.bgBlue1,
background: buttonVariantStyles(theme.colors.bgBlue1, theme.colors.bgBlue2, theme.palette.white),
};
}
};
export interface StyleProps {
theme: GrafanaTheme;
size: ComponentSize;
variant: ButtonVariant;
hasIcon: boolean;
hasText: boolean;
}
const disabledStyles = css`
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
`;
export const getButtonStyles = stylesFactory((props: StyleProps) => {
const { theme, variant } = props;
const { padding, fontSize, height } = getPropertiesForButtonSize(props);
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
return {
button: cx(
css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${theme.typography.weight.semibold};
font-family: ${theme.typography.fontFamily.sansSerif};
font-size: ${fontSize};
padding: ${padding};
height: ${height}px;
// Deduct border from line-height for perfect vertical centering on windows and linux
line-height: ${height - 2}px;
vertical-align: middle;
cursor: pointer;
border: 1px solid ${borderColor};
border-radius: ${theme.border.radius.sm};
${background};
&[disabled],
&:disabled {
${disabledStyles};
}
`,
getFocusStyle(theme),
css`
${variantStyles}
`
),
// used for buttons with icon only
iconButton: css`
padding-right: 0;
`,
iconWrap: css`
label: button-icon-wrap;
& + * {
margin-left: ${theme.spacing.sm};
}
`,
};
});
import { focusCss } from '../../themes/mixins';
import { Icon } from '../Icon/Icon';
export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'link';
......@@ -131,26 +16,28 @@ type CommonProps = {
variant?: ButtonVariant;
icon?: IconName;
className?: string;
children?: React.ReactNode;
fullWidth?: boolean;
};
export type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, icon, children, className, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
({ variant = 'primary', size = 'md', icon, fullWidth, children, className, ...otherProps }, ref) => {
const theme = useTheme();
const styles = getButtonStyles({
theme,
size: otherProps.size || 'md',
variant: variant || 'primary',
hasText: children !== undefined,
hasIcon: icon !== undefined,
size,
variant,
icon,
fullWidth,
children,
});
return (
<button className={cx(styles.button, className)} {...otherProps} ref={ref}>
<ButtonContent icon={icon} size={otherProps.size}>
{children}
</ButtonContent>
{icon && <Icon name={icon} size={size} className={styles.icon} />}
{children && <span className={styles.content}>{children}</span>}
</button>
);
}
......@@ -159,15 +46,17 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
Button.displayName = 'Button';
type ButtonLinkProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement> & AnchorHTMLAttributes<HTMLAnchorElement>;
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
({ variant, icon, children, className, disabled, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
({ variant = 'primary', size = 'md', icon, fullWidth, children, className, disabled, ...otherProps }, ref) => {
const theme = useTheme();
const styles = getButtonStyles({
theme,
size: otherProps.size || 'md',
variant: variant || 'primary',
hasText: children !== undefined,
hasIcon: icon !== undefined,
fullWidth,
size,
variant,
icon,
children,
});
const linkButtonStyles =
......@@ -186,11 +75,153 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
ref={ref}
tabIndex={disabled ? -1 : 0}
>
<ButtonContent icon={icon} size={otherProps.size}>
{children}
</ButtonContent>
{icon && <Icon name={icon} size={size} className={styles.icon} />}
{children && <span className={styles.content}>{children}</span>}
</a>
);
}
);
LinkButton.displayName = 'LinkButton';
export interface StyleProps {
size: ComponentSize;
variant: ButtonVariant;
children?: React.ReactNode;
icon?: IconName;
theme: GrafanaTheme;
fullWidth?: boolean;
narrow?: boolean;
}
const disabledStyles = css`
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
`;
export const getButtonStyles = (props: StyleProps) => {
const { theme, variant, size, children, fullWidth } = props;
const { padding, fontSize, height } = getPropertiesForButtonSize(size, theme);
const { borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
const iconOnly = !children;
return {
button: css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${theme.typography.weight.semibold};
font-family: ${theme.typography.fontFamily.sansSerif};
font-size: ${fontSize};
padding: 0 ${padding}px;
height: ${height}px;
// Deduct border from line-height for perfect vertical centering on windows and linux
line-height: ${height - 2}px;
vertical-align: middle;
cursor: pointer;
border: 1px solid ${borderColor};
border-radius: ${theme.border.radius.sm};
${fullWidth &&
`
flex-grow: 1;
justify-content: center;
`}
${variantStyles}
&[disabled],
&:disabled {
${disabledStyles};
}
`,
img: css`
width: 16px;
height: 16px;
margin-right: ${theme.spacing.sm};
margin-left: -${theme.spacing.xs};
`,
icon: css`
margin-left: -${padding / 2}px;
margin-right: ${(iconOnly ? -padding : padding) / 2}px;
`,
content: css`
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
height: 100%;
`,
};
};
function getButtonVariantStyles(from: string, to: string, textColor: string, theme: GrafanaTheme) {
return css`
background: linear-gradient(180deg, ${from} 0%, ${to} 100%);
color: ${textColor};
&:hover {
background: ${from};
color: ${textColor};
}
&:focus {
background: ${from};
outline: none;
${focusCss(theme)};
}
`;
}
function getPropertiesForVariant(theme: GrafanaTheme, variant: ButtonVariant) {
switch (variant) {
case 'secondary':
const from = theme.isLight ? theme.palette.gray7 : theme.palette.gray15;
const to = theme.isLight
? tinycolor(from)
.darken(5)
.toString()
: tinycolor(from)
.lighten(4)
.toString();
return {
borderColor: theme.isLight ? theme.palette.gray85 : theme.palette.gray25,
variantStyles: getButtonVariantStyles(
from,
to,
theme.isLight ? theme.palette.gray25 : theme.palette.gray4,
theme
),
};
case 'destructive':
return {
borderColor: theme.palette.redShade,
variantStyles: getButtonVariantStyles(
theme.palette.redBase,
theme.palette.redShade,
theme.palette.white,
theme
),
};
case 'link':
return {
borderColor: 'transparent',
variantStyles: css`
background: transparent;
color: ${theme.colors.linkExternal};
&:focus {
outline: none;
text-decoration: underline;
}
`,
};
case 'primary':
default:
return {
borderColor: theme.colors.bgBlue1,
variantStyles: getButtonVariantStyles(theme.colors.bgBlue1, theme.colors.bgBlue2, theme.palette.white, theme),
};
}
}
import React from 'react';
import { css } from 'emotion';
import { stylesFactory, useTheme } from '../../themes';
import { IconName } from '../../types/icon';
import { Icon } from '../Icon/Icon';
import { ComponentSize } from '../../types/size';
import { GrafanaTheme } from '@grafana/data';
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
content: css`
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
height: 100%;
`,
icon: css`
& + * {
margin-left: ${theme.spacing.sm};
}
`,
}));
type Props = {
icon?: IconName;
className?: string;
children: React.ReactNode;
size?: ComponentSize;
};
export function ButtonContent(props: Props) {
const { icon, children, size } = props;
const theme = useTheme();
const styles = getStyles(theme);
if (!children) {
return <span className={styles.content}>{icon && <Icon name={icon} size={size} />}</span>;
}
const iconElement = icon && (
<span className={styles.icon}>
<Icon name={icon} size={size} />
</span>
);
return (
<span className={styles.content}>
{iconElement}
<span>{children}</span>
</span>
);
}
import React, { forwardRef, HTMLAttributes } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '../../themes';
export interface Props extends HTMLAttributes<HTMLDivElement> {
noSpacing?: boolean;
}
export const ButtonGroup = forwardRef<HTMLDivElement, Props>(({ noSpacing, children, ...rest }, ref) => {
const styles = useStyles(getStyles);
const className = noSpacing ? styles.wrapperNoSpacing : styles.wrapper;
return (
<div ref={ref} className={className} {...rest}>
{children}
</div>
);
});
ButtonGroup.displayName = 'ButtonGroup';
const getStyles = (theme: GrafanaTheme) => ({
wrapper: css`
display: flex;
> a,
> button {
margin-left: ${theme.spacing.sm};
&:first-child {
margin-left: 0;
}
}
`,
wrapperNoSpacing: css`
display: flex;
> a,
> button {
border-radius: 0;
border-right: 0;
&:last-child {
border-radius: 0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0;
border-right: 1px solid ${theme.colors.border2};
}
&:first-child {
border-radius: ${theme.border.radius.sm} 0 0 ${theme.border.radius.sm};
}
}
`,
});
import React from 'react';
import { ToolbarButton, ButtonGroup, useTheme, VerticalGroup } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
export default {
title: 'Buttons/ToolbarButton',
component: ToolbarButton,
decorators: [withCenteredStory],
parameters: {},
};
export const List = () => {
const theme = useTheme();
return (
<div style={{ background: theme.colors.dashboardBg, padding: '32px' }}>
<VerticalGroup>
Wrapped in normal ButtonGroup (md spacing)
<ButtonGroup>
<ToolbarButton>Just text</ToolbarButton>
<ToolbarButton icon="sync" tooltip="Sync" />
<ToolbarButton imgSrc="./grafana_icon.svg">With imgSrc</ToolbarButton>
<ToolbarButton icon="cloud" isOpen={true}>
isOpen
</ToolbarButton>
<ToolbarButton icon="cloud" isOpen={false}>
isOpen = false
</ToolbarButton>
</ButtonGroup>
<br />
Wrapped in noSpacing ButtonGroup
<ButtonGroup noSpacing>
<ToolbarButton icon="clock-nine" tooltip="Time picker">
2020-10-02
</ToolbarButton>
<ToolbarButton icon="search-minus" />
</ButtonGroup>
<br />
Wrapped in noSpacing ButtonGroup
<ButtonGroup noSpacing>
<ToolbarButton icon="sync" />
<ToolbarButton isOpen={false} narrow />
</ButtonGroup>
</VerticalGroup>
</div>
);
};
import React, { forwardRef, HTMLAttributes } from 'react';
import { cx, css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { styleMixins, useStyles } from '../../themes';
import { IconName } from '../../types/icon';
import { Tooltip } from '../Tooltip/Tooltip';
import { Icon } from '../Icon/Icon';
export interface Props extends HTMLAttributes<HTMLButtonElement> {
/** Icon name */
icon?: IconName;
/** Tooltip */
tooltip?: string;
/** For image icons */
imgSrc?: string;
/** if true or false will show angle-down/up */
isOpen?: boolean;
/** Controls flex-grow: 1 */
fullWidth?: boolean;
/** reduces padding to xs */
narrow?: boolean;
}
export const ToolbarButton = forwardRef<HTMLButtonElement, Props>(
({ tooltip, icon, className, children, imgSrc, fullWidth, isOpen, narrow, ...rest }, ref) => {
const styles = useStyles(getStyles);
const contentStyles = cx({
[styles.content]: true,
[styles.contentWithIcon]: !!icon,
[styles.contentWithRightIcon]: isOpen !== undefined,
});
const buttonStyles = cx(
{
[styles.button]: true,
[styles.buttonFullWidth]: fullWidth,
[styles.narrow]: narrow,
},
className
);
const body = (
<button ref={ref} className={buttonStyles} {...rest}>
{icon && <Icon name={icon} size={'lg'} />}
{imgSrc && <img className={styles.img} src={imgSrc} />}
{children && <span className={contentStyles}>{children}</span>}
{isOpen === false && <Icon name="angle-down" />}
{isOpen === true && <Icon name="angle-up" />}
</button>
);
return tooltip ? (
<Tooltip content={tooltip} placement="bottom">
{body}
</Tooltip>
) : (
body
);
}
);
const getStyles = (theme: GrafanaTheme) => ({
button: css`
background: ${theme.colors.bg1};
border: 1px solid ${theme.colors.border2};
height: ${theme.height.md}px;
padding: 0 ${theme.spacing.sm};
color: ${theme.colors.textWeak};
border-radius: ${theme.border.radius.sm};
display: flex;
align-items: center;
&:focus {
outline: none;
}
&:hover {
color: ${theme.colors.text};
background: ${styleMixins.hoverColor(theme.colors.bg1, theme)};
}
`,
narrow: css`
padding: 0 ${theme.spacing.xs};
`,
img: css`
width: 16px;
height: 16px;
margin-right: ${theme.spacing.sm};
`,
buttonFullWidth: css`
flex-grow: 1;
`,
content: css`
flex-grow: 1;
`,
contentWithIcon: css`
padding-left: ${theme.spacing.sm};
`,
contentWithRightIcon: css`
padding-right: ${theme.spacing.sm};
`,
});
export * from './Button';
export { ButtonGroup } from './ButtonGroup';
export { ToolbarButton } from './ToolbarButton';
......@@ -72,6 +72,7 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
tabSelectsValue,
autoFocus = true,
} = this.props;
const combinedComponents = {
...components,
Control: ButtonComponent({ label, className, iconClass }),
......
......@@ -19,13 +19,7 @@ export interface RadioButtonProps {
}
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
const { fontSize, height, padding } = getPropertiesForButtonSize({
theme,
size,
hasIcon: false,
hasText: true,
variant: 'secondary',
});
const { fontSize, height, padding } = getPropertiesForButtonSize(size, theme);
const c = theme.palette;
const textColor = theme.colors.textSemiWeak;
......@@ -74,7 +68,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
// Deduct border from line-height for perfect vertical centering on windows and linux
line-height: ${height - 2}px;
color: ${textColor};
padding: ${padding};
padding: 0 ${padding}px;
margin-left: -1px;
border-radius: ${theme.border.radius.sm};
border: ${border};
......
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { StyleProps } from '../Button';
import { focusCss } from '../../themes/mixins';
import { ComponentSize } from '../../types/size';
export const getFocusStyle = (theme: GrafanaTheme) => css`
&:focus {
......@@ -86,30 +86,29 @@ export const inputSizesPixels = (size: string) => {
}
};
export const getPropertiesForButtonSize = (props: StyleProps) => {
const { hasText, hasIcon, size } = props;
const { spacing, typography, height } = props.theme;
export function getPropertiesForButtonSize(size: ComponentSize, theme: GrafanaTheme) {
const { typography, height, spacing } = theme;
switch (size) {
case 'sm':
return {
padding: `0 ${spacing.sm}`,
padding: spacing.base,
fontSize: typography.size.sm,
height: height.sm,
};
case 'lg':
return {
padding: `0 ${hasText ? spacing.lg : spacing.md} 0 ${hasIcon ? spacing.md : spacing.lg}`,
padding: spacing.base * 3,
fontSize: typography.size.lg,
height: height.lg,
};
case 'md':
default:
return {
padding: `0 ${hasText ? spacing.md : spacing.sm} 0 ${hasIcon ? spacing.sm : spacing.md}`,
padding: spacing.base * 2,
fontSize: typography.size.md,
height: height.md,
};
}
};
}
......@@ -18,8 +18,6 @@ export const getFormStyles = stylesFactory(
theme,
variant: options.variant,
size: options.size,
hasIcon: false,
hasText: true,
}),
input: getInputStyles({ theme, invalid: options.invalid }),
checkbox: getCheckboxStyles(theme),
......
......@@ -209,11 +209,16 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const styles = getStyles(theme);
return (
<div className={styles.wrapper}>
<FullWidthButtonContainer className={styles.addButton}>
<Button size="sm" icon="plus" onClick={() => this.onAddThreshold()} variant="secondary">
<Button
size="sm"
icon="plus"
onClick={() => this.onAddThreshold()}
variant="secondary"
className={styles.addButton}
fullWidth
>
Add threshold
</Button>
</FullWidthButtonContainer>
<div className={styles.thresholds}>
{steps
.slice(0)
......
......@@ -131,7 +131,7 @@ export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeader
// Next-gen forms
export { Form } from './Forms/Form';
export { InputControl } from './InputControl';
export * from './Button';
export { Button, LinkButton, ButtonVariant, ToolbarButton, ButtonGroup } from './Button';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { getFormStyles } from './Forms/getFormStyles';
......
......@@ -75,6 +75,7 @@ const theme: GrafanaThemeCommons = {
xxl: '1440px',
},
spacing: {
base: SPACING_BASE,
insetSquishMd: '4px 8px',
d: '16px',
xxs: '2px',
......
......@@ -84,23 +84,7 @@
}
}
// element is needed here to override font-awesome specificity
i.navbar-page-btn__folder-icon {
font-size: $font-size-sm;
color: $text-color-weak;
padding: 0 $space-sm;
position: relative;
top: -1px;
}
// element is needed here to override font-awesome specificity
i.navbar-page-btn__search {
font-size: $font-size-xs;
padding: 0 $space-xs;
}
.navbar-buttons {
// height: $navbarHeight;
display: flex;
align-items: center;
justify-content: flex-end;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment