From abe808bcfdf2e4f77c6a6f41dc8e24f2309b5934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Jan 2021 17:16:35 +0100 Subject: [PATCH] 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 * Update packages/grafana-ui/src/components/Button/ButtonGroup.tsx Co-authored-by: Alex Khomenko * 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 --- packages/grafana-data/src/types/theme.ts | 1 + .../src/components/Button/Button.story.tsx | 61 +++- .../src/components/Button/Button.tsx | 307 ++++++++++-------- .../src/components/Button/ButtonContent.tsx | 53 --- .../src/components/Button/ButtonGroup.tsx | 54 +++ .../components/Button/ToolbarButton.story.tsx | 47 +++ .../src/components/Button/ToolbarButton.tsx | 103 ++++++ .../grafana-ui/src/components/Button/index.ts | 2 + .../Forms/Legacy/Select/ButtonSelect.tsx | 1 + .../Forms/RadioButtonGroup/RadioButton.tsx | 10 +- .../src/components/Forms/commonStyles.ts | 15 +- .../src/components/Forms/getFormStyles.ts | 2 - .../ThresholdsEditorNew/ThresholdsEditor.tsx | 15 +- packages/grafana-ui/src/components/index.ts | 2 +- packages/grafana-ui/src/themes/default.ts | 1 + public/sass/components/_navbar.scss | 16 - 16 files changed, 451 insertions(+), 239 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Button/ButtonContent.tsx create mode 100644 packages/grafana-ui/src/components/Button/ButtonGroup.tsx create mode 100644 packages/grafana-ui/src/components/Button/ToolbarButton.story.tsx create mode 100644 packages/grafana-ui/src/components/Button/ToolbarButton.tsx diff --git a/packages/grafana-data/src/types/theme.ts b/packages/grafana-data/src/types/theme.ts index 0f17a1c3d0a..51349529fef 100644 --- a/packages/grafana-data/src/types/theme.ts +++ b/packages/grafana-data/src/types/theme.ts @@ -53,6 +53,7 @@ export interface GrafanaThemeCommons { }; }; spacing: { + base: number; insetSquishMd: string; d: string; xxs: string; diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx index c73414e90a5..496b728f17e 100644 --- a/packages/grafana-ui/src/components/Button/Button.story.tsx +++ b/packages/grafana-ui/src/components/Button/Button.story.tsx @@ -1,9 +1,12 @@ 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 = ({ children, ...args }) => ; -Simple.args = { - variant: 'primary', - size: 'md', - disabled: false, - children: 'Button', - icon: undefined, +export const Variants: Story = ({ children, ...args }) => { + const sizes: ComponentSize[] = ['lg', 'md', 'sm']; + const variants: ButtonVariant[] = ['primary', 'secondary', 'destructive', 'link']; + + return ( + + + {variants.map(variant => ( + + {sizes.map(size => ( + + ))} + + ))} + +
+ +
With icon and text
+ + + +
+
+ +
With icon only
+ +
+ +
Inside ButtonGroup
+ + + ); } @@ -159,15 +46,17 @@ export const Button = React.forwardRef( Button.displayName = 'Button'; type ButtonLinkProps = CommonProps & ButtonHTMLAttributes & AnchorHTMLAttributes; + export const LinkButton = React.forwardRef( - ({ 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( ref={ref} tabIndex={disabled ? -1 : 0} > - - {children} - + {icon && } + {children && {children}} ); } ); + 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), + }; + } +} diff --git a/packages/grafana-ui/src/components/Button/ButtonContent.tsx b/packages/grafana-ui/src/components/Button/ButtonContent.tsx deleted file mode 100644 index 6ff4170abc6..00000000000 --- a/packages/grafana-ui/src/components/Button/ButtonContent.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 {icon && }; - } - - const iconElement = icon && ( - - - - ); - - return ( - - {iconElement} - {children} - - ); -} diff --git a/packages/grafana-ui/src/components/Button/ButtonGroup.tsx b/packages/grafana-ui/src/components/Button/ButtonGroup.tsx new file mode 100644 index 00000000000..2eee3a67f00 --- /dev/null +++ b/packages/grafana-ui/src/components/Button/ButtonGroup.tsx @@ -0,0 +1,54 @@ +import React, { forwardRef, HTMLAttributes } from 'react'; +import { css } from 'emotion'; +import { GrafanaTheme } from '@grafana/data'; +import { useStyles } from '../../themes'; + +export interface Props extends HTMLAttributes { + noSpacing?: boolean; +} + +export const ButtonGroup = forwardRef(({ noSpacing, children, ...rest }, ref) => { + const styles = useStyles(getStyles); + const className = noSpacing ? styles.wrapperNoSpacing : styles.wrapper; + + return ( +
+ {children} +
+ ); +}); + +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}; + } + } + `, +}); diff --git a/packages/grafana-ui/src/components/Button/ToolbarButton.story.tsx b/packages/grafana-ui/src/components/Button/ToolbarButton.story.tsx new file mode 100644 index 00000000000..498cbddc988 --- /dev/null +++ b/packages/grafana-ui/src/components/Button/ToolbarButton.story.tsx @@ -0,0 +1,47 @@ +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 ( +
+ + Wrapped in normal ButtonGroup (md spacing) + + Just text + + With imgSrc + + isOpen + + + isOpen = false + + +
+ Wrapped in noSpacing ButtonGroup + + + 2020-10-02 + + + +
+ Wrapped in noSpacing ButtonGroup + + + + +
+
+ ); +}; diff --git a/packages/grafana-ui/src/components/Button/ToolbarButton.tsx b/packages/grafana-ui/src/components/Button/ToolbarButton.tsx new file mode 100644 index 00000000000..b5a6284ce44 --- /dev/null +++ b/packages/grafana-ui/src/components/Button/ToolbarButton.tsx @@ -0,0 +1,103 @@ +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 { + /** 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( + ({ 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 = ( + + ); + + return tooltip ? ( + + {body} + + ) : ( + 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}; + `, +}); diff --git a/packages/grafana-ui/src/components/Button/index.ts b/packages/grafana-ui/src/components/Button/index.ts index 8b166a86e4d..bf3af6dca9b 100644 --- a/packages/grafana-ui/src/components/Button/index.ts +++ b/packages/grafana-ui/src/components/Button/index.ts @@ -1 +1,3 @@ export * from './Button'; +export { ButtonGroup } from './ButtonGroup'; +export { ToolbarButton } from './ToolbarButton'; diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx index 8cceb55a4c4..2c0c325dca6 100644 --- a/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Forms/Legacy/Select/ButtonSelect.tsx @@ -72,6 +72,7 @@ export class ButtonSelect extends PureComponent> { tabSelectsValue, autoFocus = true, } = this.props; + const combinedComponents = { ...components, Control: ButtonComponent({ label, className, iconClass }), diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index cdc17f07661..6588b6260c6 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -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}; diff --git a/packages/grafana-ui/src/components/Forms/commonStyles.ts b/packages/grafana-ui/src/components/Forms/commonStyles.ts index d0d437f1313..c2c33814f75 100644 --- a/packages/grafana-ui/src/components/Forms/commonStyles.ts +++ b/packages/grafana-ui/src/components/Forms/commonStyles.ts @@ -1,7 +1,7 @@ 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, }; } -}; +} diff --git a/packages/grafana-ui/src/components/Forms/getFormStyles.ts b/packages/grafana-ui/src/components/Forms/getFormStyles.ts index 7ee51b0f725..70363739072 100644 --- a/packages/grafana-ui/src/components/Forms/getFormStyles.ts +++ b/packages/grafana-ui/src/components/Forms/getFormStyles.ts @@ -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), diff --git a/packages/grafana-ui/src/components/ThresholdsEditorNew/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditorNew/ThresholdsEditor.tsx index 2ef18af331f..4b22360bef5 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditorNew/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditorNew/ThresholdsEditor.tsx @@ -209,11 +209,16 @@ export class ThresholdsEditor extends PureComponent { const styles = getStyles(theme); return (
- - - +
{steps .slice(0) diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index d7b457bdad5..3beebe44b2a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts index a43b862ad5f..eab46d5135b 100644 --- a/packages/grafana-ui/src/themes/default.ts +++ b/packages/grafana-ui/src/themes/default.ts @@ -75,6 +75,7 @@ const theme: GrafanaThemeCommons = { xxl: '1440px', }, spacing: { + base: SPACING_BASE, insetSquishMd: '4px 8px', d: '16px', xxs: '2px', diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 5e14b47666b..c3b7a121904 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -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;