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.
298 lines (286 loc) • 6.32 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";
const intentStyleMap = ({ tokens }) => ({
primary: {
color: tokens.buttonPrimaryColor,
defaultForeground: tokens.buttonPrimaryForeground,
highlight: tokens.buttonPrimaryHoverBackground,
activeBackground: tokens.buttonPrimaryActiveBackground
},
"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 = false,
intent = "primary",
variant = "default",
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}px ${
theme.tokens.buttonPaddingHorizontal
}px`,
textAlign: "center",
fontSize: 15,
...(loading && { position: "relative" }),
lineHeight: 1.2,
transition: "all 0.3s ease-out",
transitionProperty: "background, fill, stroke, color, border-color",
fontWeight: 400,
":focus": {
outline: `2px solid ${color}33`,
":active": {
background: activeBackground
}
},
extend: [
{
condition: !disabled,
style: {
extend: [
{
condition: variant === "default",
style: {
borderWidth: 1,
borderStyle: "solid",
borderColor: color,
background: color,
color: defaultForeground,
fill: defaultForeground,
stroke: defaultForeground,
":hover": {
background: highlight
}
}
},
{
condition: variant === "outline",
style: {
borderWidth: 1,
borderStyle: "solid",
borderColor: color,
color: color,
fill: color,
stroke: color,
":hover": {
background: color,
color: defaultForeground,
borderColor: highlight,
fill: defaultForeground,
stroke: defaultForeground
}
}
},
{
condition: variant === "text",
style: {
borderWidth: 1,
borderStyle: "solid",
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: "lightgrey",
color: "lightgrey",
stroke: "lightgrey",
":focus": {
outline: "2px solid #CCCCCC33"
},
extend: [
{
condition: variant === "default",
style: {
background: "lightgrey",
color: "grey",
stroke: "grey",
fill: "grey"
}
},
{
condition: variant === "outline",
style: {
borderWidth: 1,
borderStyle: "solid",
borderColor: "lightgrey"
}
}
]
}
}
]
};
};
export function Button({
children,
intent,
variant,
loading,
disabled,
arrow,
iconBefore,
iconAfter,
...props
}) {
const { theme } = useFela();
const styleProps = {
disabled,
intent,
variant,
loading,
theme,
arrow
};
return (
<Click
disabled={disabled || loading}
{...props}
extend={[button(styleProps), getThemeStyle("button", theme, styleProps)]}
>
{loading && (
<Block
extend={{
position: "absolute",
margin: "0 auto",
top: 10
}}
>
<Spinner color="auto" size={30} />
</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.propTypes = {
/** A JSX node */
children: PropTypes.node,
intent: PropTypes.oneOf([
"primary",
"primary-light",
"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
};