vcc-ui
Version:
VCC UI is a collection of React UI Components that can be used for developing front-end applications at Volvo Car Corporation.
359 lines (344 loc) • 9.33 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { useFela } from 'react-fela';
import { Click } from '../click';
import { Block } from '../block';
import { Arrow } from '../arrow';
import { Icon } from '../icon';
import { Inline } from '../inline';
import { Spinner } from '../spinner';
import { getThemeStyle } from '../../get-theme-style';
import { sizeToRem } from '../../size-to-rem';
import { deprecate } from '../../deprecate';
const padding = (x, y) => ({
padding: `${x}px ${y}px`,
});
const intentStyleMap = ({ tokens }) => ({
primary: {
color: tokens.buttonPrimaryColor,
defaultForeground: tokens.buttonPrimaryForeground,
highlight: tokens.buttonPrimaryHoverBackground,
activeBackground: tokens.buttonPrimaryActiveBackground,
},
// deprecated, remove primary light in in 2.x
'primary-light': {
color: tokens.buttonPrimaryLightColor,
defaultForeground: tokens.buttonPrimaryLightForeground,
activeBackground: tokens.buttonPrimaryLightActiveBackground,
},
secondary: {
color: tokens.buttonSecondaryColor,
defaultForeground: tokens.buttonSecondaryForeground,
activeBackground: tokens.buttonSecondaryActiveBackground,
},
destructive: {
color: tokens.buttonDestructiveColor,
defaultForeground: tokens.buttonDestructiveForeground,
highlight: tokens.buttonDestructiveHoverBackground,
activeBackground: tokens.buttonDestructiveActiveBackground,
},
});
const arrowTransform = {
up: 'translateY(-3px)',
down: 'translateY(3px)',
right: 'translateX(5px)',
left: 'translateX(-5px)',
};
const button = ({ disabled, intent, variant, size, loading, arrow, theme }) => {
const {
color,
defaultForeground,
activeBackground,
highlight,
} = intentStyleMap(theme)[intent];
return {
display: 'inline-flex',
flexShrink: 0,
justifyContent: 'center',
alignItems: 'baseline',
...padding(
theme.tokens.buttonPaddingVertical,
theme.tokens.buttonPaddingHorizontal
),
textAlign: 'center',
fontSize: sizeToRem(2),
lineHeight: 1.2,
transitionDuration: disabled ? 0 : '300ms',
transitionTimingFunction: 'ease-out',
transitionProperty: 'background, fill, stroke, color, border-color',
borderColor: 'transparent',
borderStyle: 'solid',
borderWidth: 1,
fontWeight: 400,
':focus': {
outline: `2px solid ${color}33`,
':active': {
background: activeBackground,
},
},
extend: [
{
condition: size === 'small',
style: {
fontSize: sizeToRem(1.5),
...padding(
theme.tokens.buttonPaddingVertical - theme.baselineGrid,
theme.tokens.buttonPaddingHorizontal - theme.baselineGrid * 3
),
'> div svg': {
height: theme.baselineGrid * 3,
width: theme.baselineGrid * 3,
},
},
},
{
condition: size === 'large',
style: {
fontSize: sizeToRem(3),
...padding(
theme.tokens.buttonPaddingVertical + theme.baselineGrid,
theme.tokens.buttonPaddingHorizontal + theme.baselineGrid * 2
),
'> div svg': {
height: theme.baselineGrid * 5,
width: theme.baselineGrid * 5,
},
},
},
{
condition: loading,
style: {
justifyContent: 'center',
alignItems: 'center',
},
},
{
condition: !disabled,
style: {
extend: [
{
condition: variant === 'default',
style: {
borderColor: color,
background: color,
color: defaultForeground,
fill: defaultForeground,
stroke: defaultForeground,
':hover': {
borderColor: highlight,
background: highlight,
},
},
},
{
condition: variant === 'outline',
style: {
borderColor: color,
color: color,
fill: color,
stroke: color,
':hover': {
background: color,
color: defaultForeground,
borderColor: highlight,
fill: defaultForeground,
stroke: defaultForeground,
},
},
},
{
condition: variant === 'text',
style: {
borderColor: 'transparent',
fill: color,
color: color,
stroke: color,
':hover': {
color: highlight,
},
},
},
{
condition: arrow,
style: {
':hover svg': {
transform: arrowTransform[arrow],
},
},
},
],
},
},
{
condition: loading,
style: {
pointerEvents: 'none',
},
},
{
condition: disabled,
style: {
cursor: 'not-allowed',
fill: theme.color.foreground.secondary,
color: theme.color.foreground.secondary,
stroke: theme.color.foreground.secondary,
':focus': {
outline: '2px solid #CCCCCC33',
},
extend: [
{
condition: variant === 'default',
style: {
borderColor: 'lightgrey',
background: 'lightgrey',
color: 'grey',
stroke: 'grey',
fill: 'grey',
},
},
{
condition: variant === 'outline',
style: {
borderColor: 'lightgrey',
},
},
],
},
},
],
};
};
export const Button = React.forwardRef(
(
{
children,
loading,
arrow,
iconBefore,
iconAfter,
disabled,
intent = 'primary',
variant = 'default',
size = 'medium',
...props
},
ref
) => {
const { theme } = useFela();
const styleProps = {
disabled,
intent,
variant,
loading,
size,
theme,
arrow,
};
deprecate(
'intent=primary-light deprecated, use dark theme instead',
intent === 'primary-light'
);
deprecate(
'iconBefore deprecated, use Icon component and pass as child',
iconBefore
);
deprecate(
'iconAfter deprecated, use Icon component and pass as child',
iconAfter
);
deprecate(
'Arrow deprecated, arrows will no longer be part of new button component design',
arrow
);
return (
<Click
ref={ref}
{...props}
disabled={disabled || loading}
extend={[
button(styleProps),
getThemeStyle('button', theme, styleProps),
]}
>
{loading && (
<Block
extend={{
position: 'absolute',
margin: '0 auto',
}}
>
<Spinner color="inherit" size={theme.baselineGrid * 4} />
</Block>
)}
{loading ? (
<Inline extend={{ visibility: 'hidden' }}>
<InnerButton
iconBefore={iconBefore}
iconAfter={iconAfter}
arrow={arrow}
>
{children}
</InnerButton>
</Inline>
) : (
<InnerButton
iconBefore={iconBefore}
iconAfter={iconAfter}
arrow={arrow}
>
{children}
</InnerButton>
)}
</Click>
);
}
);
const InnerButton = ({ children, iconBefore, iconAfter, arrow }) => (
<React.Fragment>
{iconBefore && (
<Inline
extend={{ marginRight: 12, position: 'relative', bottom: '-3px' }}
>
<Icon size="m" type={iconBefore} />
</Inline>
)}
{arrow && arrow === 'left' && (
<Inline extend={{ marginRight: 8 }}>
<Arrow direction={'left'} />
</Inline>
)}
{children}
{arrow && arrow !== 'left' && (
<Inline extend={{ marginLeft: 8 }}>
<Arrow direction={arrow} />
</Inline>
)}
{iconAfter && (
<Inline extend={{ marginLeft: 12, position: 'relative', bottom: '-3px' }}>
<Icon size="m" type={iconAfter} />
</Inline>
)}
</React.Fragment>
);
Button.displayName = 'Button';
Button.propTypes = {
/** A JSX node */
children: PropTypes.node,
intent: PropTypes.oneOf([
'primary',
'primary-light', // deprecated
'secondary',
'destructive',
]),
variant: PropTypes.oneOf(['default', 'outline', 'text']),
loading: PropTypes.bool,
disabled: PropTypes.bool,
arrow: PropTypes.oneOf(['up', 'down', 'right', 'left']),
/** Add an icon before button text */
iconBefore: PropTypes.string,
/** Add an icon after button text */
iconAfter: PropTypes.string,
/** Changes the rendered element from `<button>` to `<a>` */
href: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
};