UNPKG

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
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']), };