Files
grafana/packages/grafana-ui/src/components/Button/AbstractButton.tsx
T
Dominik Prokop e40dd982c6 Docs: Add styling.md with guide to Emotion at Grafana (#19411)
* Add styling.md with guide to emotion

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update PR guide

* Add stylesFactory helper function

* Simplify styles creator signature

* Make styles factory deps optional

* Update typing

* First batch of updates

* Remove unused import

* Update tests

(cherry picked from commit 75b21c7603)
2019-10-16 12:32:09 +03:00

212 lines
5.8 KiB
TypeScript

import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import tinycolor from 'tinycolor2';
import { css, cx } from 'emotion';
import { Themeable, GrafanaTheme } from '../../types';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { stylesFactory } from '../../themes/stylesFactory';
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 = (
from: string,
to: string,
textColor: string,
textShadowColor = 'rgba(0, 0, 0, 0.1)',
invert = false
) => css`
background: linear-gradient(to bottom, ${from}, ${to});
color: ${textColor};
text-shadow: 0 ${invert ? '1px' : '-1px'} ${textShadowColor};
&:hover {
background: ${from};
color: ${textColor};
}
&:focus {
background: ${from};
outline: none;
}
`;
interface StyleDeps {
theme: GrafanaTheme;
size: ButtonSize;
variant: ButtonVariant;
withIcon: boolean;
}
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
const borderRadius = theme.border.radius.sm;
let padding,
background,
fontSize,
iconDistance,
height,
fontWeight = theme.typography.weight.semibold;
switch (size) {
case 'sm':
padding = `${theme.spacing.xs} ${theme.spacing.sm}`;
fontSize = theme.typography.size.sm;
iconDistance = theme.spacing.xs;
height = theme.height.sm;
break;
case 'md':
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
fontSize = theme.typography.size.md;
iconDistance = theme.spacing.sm;
height = theme.height.md;
break;
case 'lg':
padding = `${theme.spacing.md} ${theme.spacing.lg}`;
fontSize = theme.typography.size.lg;
fontWeight = theme.typography.weight.regular;
iconDistance = theme.spacing.sm;
height = theme.height.lg;
break;
default:
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
iconDistance = theme.spacing.sm;
fontSize = theme.typography.size.base;
height = theme.height.md;
}
switch (variant) {
case 'primary':
background = buttonVariantStyles(theme.colors.greenBase, theme.colors.greenShade, theme.colors.white);
break;
case 'secondary':
background = buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white);
break;
case 'danger':
background = buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white);
break;
case 'inverse':
const from = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type) as string;
const to = selectThemeVariant(
{
light: tinycolor(from)
.darken(5)
.toString(),
dark: tinycolor(from)
.lighten(4)
.toString(),
},
theme.type
) as string;
background = buttonVariantStyles(from, to, theme.colors.link, 'rgba(0, 0, 0, 0.1)', true);
break;
case 'transparent':
background = css`
${buttonVariantStyles('', '', theme.colors.link, 'rgba(0, 0, 0, 0.1)', true)};
background: transparent;
`;
break;
}
return {
button: css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${fontWeight};
font-size: ${fontSize};
font-family: ${theme.typography.fontFamily.sansSerif};
line-height: ${theme.typography.lineHeight.xs};
padding: ${padding};
text-align: ${withIcon ? 'left' : 'center'};
vertical-align: middle;
cursor: pointer;
border: none;
height: ${height};
border-radius: ${borderRadius};
${background};
&[disabled],
&:disabled {
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
}
`,
iconWrap: css`
label: button-icon-wrap;
display: flex;
align-items: center;
`,
icon: css`
label: button-icon;
margin-right: ${iconDistance};
filter: brightness(100);
`,
};
});
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
renderAs,
theme,
size = 'md',
variant = 'primary',
className,
icon,
children,
...otherProps
}) => {
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
const nonHtmlProps = {
theme,
size,
variant,
};
const finalClassName = cx(buttonStyles.button, className);
const finalChildren = icon ? (
<span className={buttonStyles.iconWrap}>
<i className={cx([icon, buttonStyles.icon])} />
<span>{children}</span>
</span>
) : (
children
);
const finalProps =
typeof renderAs === 'string'
? {
...otherProps,
className: finalClassName,
children: finalChildren,
}
: {
...otherProps,
...nonHtmlProps,
className: finalClassName,
children: finalChildren,
};
return React.createElement(renderAs, finalProps);
};
AbstractButton.displayName = 'AbstractButton';