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 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