Skip to content
This repository has been archived by the owner on Jan 20, 2022. It is now read-only.

Commit

Permalink
Add loading spinner (#75)
Browse files Browse the repository at this point in the history
* Add loading spinner

* Make demo groups shared

* Loader story

* Add key and remove unused declaration

* Switch DOM prperties to camelCase

* Change to theme

* Color and type comments

* Change sizes

* README/storybook changes

* Prettier
  • Loading branch information
jgzuke authored Aug 23, 2019
1 parent 16a47a1 commit b40cfef
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 107 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
- [Typography](#typography)
- [Buttons](#buttons)
- [Modals](#modals)
- [Loaders](#loaders)
- [Spinners](#spinners)
- [Emotion Example](#emotion-example)
- [Developing Space Kit](#developing-space-kit)
- [Releases](#releases)
Expand Down Expand Up @@ -292,6 +294,34 @@ export const PrimaryButton: React.FC<ComponentProps<typeof Button>> = ({
### Modals
### Loaders
Zeplin: https://app.zeplin.io/project/5c7dcb5ab4e654bca8cde54d/screen/5d56d40acf2df541b112fc57
#### Spinners
Spinners are used when we are unable to determine loading time. Ideally, they should appear as briefly and infrequently as possible.
Spinners can be configured through their `theme` and `size` props. By default a loading spinner will have a 'light' theme and 'medium' size.
You can configure anything you'd like with tailwind or emotion, but you should never have to do this.
#### Example
```js
import React from "react";
import { LoadingSpinner } from "@apollo/space-kit/Loaders";
export const LoadingPage: React.FC<Props> = (otherProps) => (
<div {...otherProps} >
<LoadingSpinner
theme="light"
size="medium"
/>
</div>
);
```
### Emotion Example
```js
Expand Down
94 changes: 29 additions & 65 deletions src/Button/Button.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { storiesOf } from "@storybook/react";
import { Button } from "./Button";
import { IconShip2 } from "../icons/IconShip2";
import { colors } from "../colors";
import * as typography from "../typography";
import { DemoSection } from "../shared/DemoSection";
import { DemoSection, DemoGroup, DemoGroupProps } from "../shared/DemoSection";

const iconElement = <IconShip2 css={{ width: "100%", height: "100%" }} />;

Expand Down Expand Up @@ -45,83 +44,48 @@ interface AllowedCss {
color: CSS.ColorProperty;
}

const VerticalButtonGroup: React.FC<{
buttonCss?: AllowedCss;
buttonProps?: Partial<Omit<ComponentProps<typeof Button>, "children">>;
children: JSX.Element | JSX.Element[];
darkButtonCss?: AllowedCss;
title: string;
description?: string;
}> = ({
const VerticalButtonGroup: React.FC<
{
buttonCss?: AllowedCss;
buttonProps?: Partial<Omit<ComponentProps<typeof Button>, "children">>;
darkButtonCss?: AllowedCss;
} & DemoGroupProps
> = ({
buttonProps = {},
children,
title,
buttonCss = {},
darkButtonCss = {},
description,
...otherProps
}) => (
<div {...otherProps} css={{ margin: "0 20px", width: 300 }}>
<hr
style={{
height: 1,
borderWidth: 0,
backgroundColor: colors.silver.dark,
marginBottom: 24,
}}
/>
<div
<DemoGroup {...otherProps}>
<ButtonWrapper>
{React.Children.map(children, child => (
<div css={{ margin: 12 }}>
{cloneElement(child as any, {
...buttonProps,
css: buttonCss,
})}
</div>
))}
</ButtonWrapper>
<ButtonWrapper
css={{
...typography.base.base,
textTransform: "uppercase",
fontWeight: 600,
margin: 6,
backgroundColor: colors.black.base,
}}
>
{title}
</div>

{description && (
<div
css={{
...typography.base.small,
margin: 6,
}}
>
{description}
</div>
)}

<div css={{ display: "flex", flexWrap: "wrap" }}>
<ButtonWrapper>
{React.Children.map(children, child => (
{React.Children.map(children, child => {
return (
<div css={{ margin: 12 }}>
{cloneElement(child as any, {
...buttonProps,
css: buttonCss,
theme: "dark",
css: { ...buttonCss, ...darkButtonCss },
})}
</div>
))}
</ButtonWrapper>
<ButtonWrapper
css={{
backgroundColor: colors.black.base,
}}
>
{React.Children.map(children, child => {
return (
<div css={{ margin: 12 }}>
{cloneElement(child as any, {
...buttonProps,
theme: "dark",
css: { ...buttonCss, ...darkButtonCss },
})}
</div>
);
})}
</ButtonWrapper>
</div>
</div>
);
})}
</ButtonWrapper>
</DemoGroup>
);

const DummyNavLink: React.FC<{ to: string; className?: string }> = ({
Expand Down
112 changes: 112 additions & 0 deletions src/Loaders/Loaders.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/** @jsx jsx */
import { jsx } from "@emotion/core";
import { storiesOf } from "@storybook/react";
import { LoadingSpinner, Size } from "./index";
import { Button } from "../Button";
import { colors } from "../colors";
import * as typography from "../typography";
import { DemoSection, DemoGroup } from "../shared/DemoSection";

const SPINNER_SIZES: Size[] = ["large", "medium", "small", "xsmall", "2xsmall"];

storiesOf("Loaders", module)
.addParameters({
options: {
showPanel: false,
},
})
.add("Catalog", () => (
<div css={{ color: colors.black.base }}>
<DemoSection
title="Spinners"
description="Spinners are used when we are unable to determine loading time. Ideally, they should appear as briefly and infrequently as possible."
>
<DemoGroup
title="On Pages & Cards"
description="Spinners will most often appear on full pages and cards. Whenever possible, they should be paired with descriptive text that helps the user understand exactly what the system is working toward. Spinners should be center-aligned to the page with any text spaced 20 px below."
>
<div
css={{
flex: "1 1 0%",
border: `1px solid ${colors.silver.dark}`,
borderRadius: 8,
margin: 6,
}}
>
{SPINNER_SIZES.map(size => (
<div key={size} css={{ margin: 20 }}>
<span
css={{
...typography.base.small,
fontWeight: 600,
display: "block",
textTransform: "uppercase",
}}
>
{size}
</span>
<LoadingSpinner css={{ display: "block" }} size={size} />
</div>
))}
</div>
<div
css={{
backgroundColor: colors.black.base,
flex: "1 1 0%",
border: `1px solid ${colors.silver.dark}`,
borderRadius: 8,
margin: 6,
}}
>
{SPINNER_SIZES.map(size => (
<div key={size} css={{ margin: 20 }}>
<span
css={{
...typography.base.small,
fontWeight: 600,
color: colors.grey.lighter,
display: "block",
textTransform: "uppercase",
}}
>
{size}
</span>
<LoadingSpinner
theme="dark"
css={{ display: "block" }}
size={size}
/>
</div>
))}
</div>
</DemoGroup>
<DemoGroup
title="In Buttons"
description="Spinners may also exist inside buttons. If a user clicks on a button and the system needs time to process the request, the button should expand horizontally to the right, the color should change to the next lightest shade, and the spinner should appear to the left of the text."
>
<Button
color={colors.blue.base}
icon={<LoadingSpinner theme="dark" size="2xsmall" />}
css={{ width: "100%", marginTop: 6, marginBottom: 20 }}
>
Submit
</Button>
<Button
color={colors.blue.base}
disabled
icon={<LoadingSpinner size="2xsmall" />}
css={{ width: "100%", marginBottom: 20 }}
>
Submit
</Button>
<Button
feel="flat"
icon={<LoadingSpinner size="2xsmall" />}
css={{ width: "100%" }}
>
Submit
</Button>
</DemoGroup>
</DemoSection>
</div>
));
112 changes: 112 additions & 0 deletions src/Loaders/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/** @jsx jsx */
import React from "react";
import * as CSS from "csstype";
import { jsx, keyframes } from "@emotion/core";
import { colors } from "../colors";

export type Size = "large" | "medium" | "small" | "xsmall" | "2xsmall";
export type Theme = "light" | "dark";
interface Props {
/**
* Class name that will be applied to the svg
*/
className?: string;

/**
* Theme for the spinner
* @default "light"
*/
theme?: Theme;

/**
* Size of the spinner
* @default "medium"
*/
size?: Size;
}

// The whole animation is exactly 5 seconds long.
// Each rotation is 450 degrees, with a deceleration at
// 420 degrees, and a reduced speed between 420-450.
// 1st rotation: 60 deg and 90 deg
// 2nd rotation: 150 deg and 180 deg
// 3rd rotation: 240 deg and 270 deg
// 4th rotation: 330 deg and 360 deg (restart loop)
const SPIN = keyframes`
25% { transform: rotate(450deg) }
50% { transform: rotate(900deg) }
75% { transform: rotate(1350deg) }
100% { transform: rotate(1800deg) }
`;

const SIZE_MAP: Record<Size, number> = {
large: 90,
medium: 64,
small: 48,
xsmall: 32,
"2xsmall": 16,
};

const THEME_MAP: Record<
Theme,
{
orbitColor: CSS.ColorProperty;
orbitOpacity: CSS.GlobalsNumber;
asteroidColor: CSS.ColorProperty;
}
> = {
light: {
orbitColor: colors.silver.light,
orbitOpacity: 1,
asteroidColor: colors.blue.base,
},
dark: {
orbitColor: colors.white,
orbitOpacity: 0.5,
asteroidColor: colors.white,
},
};

export const LoadingSpinner: React.FC<Props> = ({
theme = "light",
size = "medium",
className,
...props
}) => {
const { orbitColor, orbitOpacity, asteroidColor } = THEME_MAP[theme];

const pixelSize = SIZE_MAP[size];

return (
<svg
className={className}
viewBox="0 0 100 100"
css={{
width: pixelSize,
height: pixelSize,
}}
{...props}
>
<circle
strokeWidth="8"
stroke={orbitColor}
strokeOpacity={orbitOpacity}
fill="transparent"
r="41"
cx="50"
cy="50"
/>
<g transform="translate(50 50)">
<circle
css={{
animation: `${SPIN} 5s cubic-bezier(0.6, 0.22, 0.44, 0.8) infinite`,
}}
fill={asteroidColor}
r="10"
cx="40"
cy="0"
/>
</g>
</svg>
);
};
1 change: 1 addition & 0 deletions src/Loaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./LoadingSpinner";
Loading

0 comments on commit b40cfef

Please sign in to comment.