Skip to content

Commit

Permalink
Add loading prop to Button (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
moroshko authored Sep 17, 2020
1 parent 5a53e18 commit 09c98ae
Show file tree
Hide file tree
Showing 27 changed files with 444 additions and 318 deletions.
65 changes: 54 additions & 11 deletions src/components/Button.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from "react";
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import useBackground from "../hooks/useBackground";
import LoadingIcon from "./LoadingIcon";
import useTheme from "../hooks/useTheme";
import useBackground, { mapResponsiveValues } from "../hooks/useBackground";
import useResponsivePropsCSS from "../hooks/useResponsivePropsCSS";
import {
responsiveMarginType,
responsiveWidthType,
} from "../hooks/useResponsiveProp";
import { hasOwnProperty } from "../utils/core";
import { getPropsFromMap } from "../utils/component";
import { responsiveMargin, responsiveSize } from "../utils/css";
import { mergeProps } from "../utils/component";
import { formatArray } from "../utils/array";
Expand All @@ -18,6 +21,7 @@ const TYPES = ["button", "submit"];
const DEFAULT_PROPS = {
variant: "primary",
color: "highlight.blue.t100",
loading: false,
disabled: false,
type: "button",
__internal__keyboardFocus: false,
Expand Down Expand Up @@ -49,15 +53,20 @@ const mediumColorsMap = {
"secondary.turquoise.t10": true,
};

function getInheritedColor(backgroundColor) {
return darkColorsMap[backgroundColor]
function getButtonColor(bg) {
return darkColorsMap[bg]
? "white"
: mediumColorsMap[backgroundColor]
: mediumColorsMap[bg]
? "black"
: "highlight.blue.t100";
}

function getLoadingIconColor(bg) {
return getButtonColor(bg);
}

function Button(props) {
const theme = useTheme();
const { bgMap } = useBackground();
const mergedProps = mergeProps(
props,
Expand All @@ -71,6 +80,8 @@ function Button(props) {
}
);
const {
variant,
loading,
disabled,
type,
onClick,
Expand All @@ -80,12 +91,15 @@ function Button(props) {
__internal__hover,
__internal__active,
} = mergedProps;
const css = useResponsivePropsCSS(mergedProps, DEFAULT_PROPS, {
const showLoadingIcon =
["primary", "secondary"].includes(variant) &&
props.color !== "green" &&
loading;
const buttonCSS = useResponsivePropsCSS(mergedProps, DEFAULT_PROPS, {
color: (_, theme, bp) => {
const { variant } = mergedProps;
let color = hasOwnProperty(props, "color")
? mergedProps.color
: getInheritedColor(bgMap?.[bp]);
: getButtonColor(bgMap?.[bp]);

if (
(color === "black" && variant !== "secondary") ||
Expand All @@ -95,8 +109,10 @@ function Button(props) {
}

return theme.button.getCSS({
targetElement: "button",
variant,
color,
showLoadingIcon,
__internal__keyboardFocus,
__internal__hover,
__internal__active,
Expand All @@ -105,16 +121,42 @@ function Button(props) {
margin: responsiveMargin,
width: responsiveSize("width"),
});
const loadingIconCSS = theme.button.getCSS({
targetElement: "loadingIcon",
});
const contentCSS = theme.button.getCSS({
targetElement: "content",
showLoadingIcon,
});
const loadingIconColorProps = useMemo(
() =>
getPropsFromMap(
"color",
mapResponsiveValues(
bgMap,
(bg) => {
return getLoadingIconColor(bg);
},
theme
)
),
[bgMap, theme]
);

return (
<button
css={css}
disabled={disabled}
css={buttonCSS}
disabled={showLoadingIcon || disabled}
type={type}
onClick={onClick}
data-testid={testId}
>
{children}
{showLoadingIcon && (
<span css={loadingIconCSS} aria-hidden="true">
<LoadingIcon {...loadingIconColorProps} />
</span>
)}
<span css={contentCSS}>{children}</span>
</button>
);
}
Expand Down Expand Up @@ -147,6 +189,7 @@ Button.propTypes = {
);
}
},
loading: PropTypes.bool,
disabled: PropTypes.bool,
type: PropTypes.oneOf(TYPES),
onClick: PropTypes.func,
Expand Down
16 changes: 16 additions & 0 deletions src/components/Button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ describe("Button", () => {
});
});

it("loading", () => {
render(<Button loading>I agree</Button>);

const button = screen.getByRole("button");

expect(button).toHaveStyle({
backgroundColor: "transparent",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "rgba(0,0,0,0.35)",
cursor: "progress",
});

expect(screen.getByLabelText("Loading icon")).toBeInTheDocument();
});

it("disabled", () => {
render(<Button disabled>Find out more</Button>);

Expand Down
45 changes: 17 additions & 28 deletions src/components/LoadingIcon.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import { keyframes } from "@emotion/core";
import useTheme from "../hooks/useTheme";
import { responsivePropType } from "../hooks/useResponsiveProp";
import useResponsivePropsCSS from "../hooks/useResponsivePropsCSS";

const SIZES = ["small", "medium", "large"];
const COLORS = ["highlight.blue.t100", "white"];
const COLORS = ["highlight.blue.t100", "white", "black"];

const DEFAULT_PROPS = {
size: "small",
Expand All @@ -23,10 +24,8 @@ LoadingIcon.DEFAULT_PROPS = DEFAULT_PROPS;

function LoadingIcon(_props) {
const props = { ...DEFAULT_PROPS, ..._props };
const { size, color, testId } = props;
const theme = useTheme();
const { size, testId } = props;
const radius = circleRadiusMap[size] ?? 4;
const circleColor = theme.getColor(color) ?? theme.colors.black;
const stepPx = `${3 * radius}px`;
const frames = 12; // 3 circles * 4 steps each
const percantagePerFrame = 100 / frames;
Expand Down Expand Up @@ -71,7 +70,14 @@ function LoadingIcon(_props) {
}, {})
);

const css = [
const svgCSS = useResponsivePropsCSS(props, DEFAULT_PROPS, {
color: ({ color }, theme) => {
return {
fill: theme.getColor(color) ?? theme.colors.black,
};
},
});
const circleCSS = [
getKeyframes([
start,
right,
Expand Down Expand Up @@ -120,6 +126,7 @@ function LoadingIcon(_props) {

return (
<svg
css={svgCSS}
width={svgSize}
height={svgSize}
viewBox={`0 0 ${svgSize} ${svgSize}`}
Expand All @@ -128,34 +135,16 @@ function LoadingIcon(_props) {
aria-label="Loading icon"
data-testid={testId}
>
<circle
cx={radius}
cy={radius}
r={radius}
fill={circleColor}
css={css[0]}
/>
<circle
cx={radius}
cy={4 * radius}
r={radius}
fill={circleColor}
css={css[1]}
/>
<circle
cx={4 * radius}
cy={4 * radius}
r={radius}
fill={circleColor}
css={css[2]}
/>
<circle cx={radius} cy={radius} r={radius} css={circleCSS[0]} />
<circle cx={radius} cy={4 * radius} r={radius} css={circleCSS[1]} />
<circle cx={4 * radius} cy={4 * radius} r={radius} css={circleCSS[2]} />
</svg>
);
}

LoadingIcon.propTypes = {
...responsivePropType("color", PropTypes.oneOf(COLORS)),
size: PropTypes.oneOf(SIZES),
color: PropTypes.oneOf(COLORS),
testId: PropTypes.string,
};

Expand Down
Loading

1 comment on commit 09c98ae

@vercel
Copy link

@vercel vercel bot commented on 09c98ae Sep 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.