Skip to content

Commit fd21206

Browse files
authored
Loading state for button (codesandbox#3894)
* refactor to element syntax * add types for ref * create bare bones loading * add loading state for button * & element props * use a link instead of button in conflicting modals * add stories for size * was missing a line about padding
1 parent 28ce406 commit fd21206

File tree

7 files changed

+125
-62
lines changed

7 files changed

+125
-62
lines changed

packages/app/src/app/pages/common/Modals/CommitModal/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { FunctionComponent } from 'react';
2-
import { Stack, Button, Text } from '@codesandbox/components';
2+
import { Stack, Link, Text } from '@codesandbox/components';
33
import css from '@styled-system/css';
44

55
import { GitProgress } from 'app/components/GitProgress';
@@ -29,8 +29,7 @@ const CommitModal: FunctionComponent = () => {
2929
instead.
3030
</Text>
3131
<Stack justify="flex-end">
32-
<Button
33-
as="a"
32+
<Link
3433
css={css({
3534
textDecoration: 'none',
3635
width: 'auto',
@@ -40,7 +39,7 @@ const CommitModal: FunctionComponent = () => {
4039
rel="noreferrer noopener"
4140
>
4241
Open a PR
43-
</Button>
42+
</Link>
4443
</Stack>
4544
</>
4645
);

packages/app/src/app/pages/common/Modals/LiveSessionEnded/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { FunctionComponent } from 'react';
22

3-
import { Button, Stack } from '@codesandbox/components';
3+
import { Button, Link, Stack } from '@codesandbox/components';
44
import { useOvermind } from 'app/overmind';
55
import css from '@styled-system/css';
66
import { Alert } from '../Common/Alert';
@@ -56,8 +56,7 @@ export const LiveSessionEnded: FunctionComponent = () => {
5656
Fork Sandbox
5757
</Button>
5858
)}
59-
<Button
60-
as="a"
59+
<Link
6160
href="/s"
6261
css={css({
6362
width: 'auto',
@@ -66,7 +65,7 @@ export const LiveSessionEnded: FunctionComponent = () => {
6665
onClick={modalClosed}
6766
>
6867
Create Sandbox
69-
</Button>
68+
</Link>
7069
</Stack>
7170
</Alert>
7271
);

packages/app/src/app/pages/common/Modals/PRModal/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { FunctionComponent } from 'react';
22

33
import { GitProgress } from 'app/components/GitProgress';
44
import { useOvermind } from 'app/overmind';
5-
import { Stack, Button, Text } from '@codesandbox/components';
5+
import { Stack, Link, Text } from '@codesandbox/components';
66
import css from '@styled-system/css';
77

88
export const PRModal: FunctionComponent = () => {
@@ -24,8 +24,7 @@ export const PRModal: FunctionComponent = () => {
2424
</Text>
2525

2626
<Stack justify="flex-end">
27-
<Button
28-
as="a"
27+
<Link
2928
css={css({
3029
width: 'auto',
3130
textDecoration: 'none',
@@ -35,7 +34,7 @@ export const PRModal: FunctionComponent = () => {
3534
target="_blank"
3635
>
3736
Click here if nothing happens.
38-
</Button>
37+
</Link>
3938
</Stack>
4039
</>
4140
)

packages/components/src/components/Button/button.stories.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,20 @@ export const Disabled = () => (
3535
</Button>
3636
</Stack>
3737
);
38+
39+
export const Loading = () => (
40+
<Stack direction="vertical" gap={4} style={{ width: 200 }}>
41+
<Button loading variant="primary">
42+
primary by default
43+
</Button>
44+
<Button loading variant="secondary">
45+
Save as Template
46+
</Button>
47+
<Button loading variant="link">
48+
Open sandbox
49+
</Button>
50+
<Button loading variant="danger">
51+
Go live
52+
</Button>
53+
</Stack>
54+
);

packages/components/src/components/Button/index.tsx

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import styled from 'styled-components';
2-
import css from '@styled-system/css';
1+
import React from 'react';
32
import deepmerge from 'deepmerge';
4-
import { Element } from '../Element';
3+
import styled, { keyframes } from 'styled-components';
4+
import VisuallyHidden from '@reach/visually-hidden';
5+
import { Element, IElementProps } from '../Element';
56

67
const variantStyles = {
78
primary: {
@@ -57,57 +58,97 @@ const variantStyles = {
5758
},
5859
};
5960

61+
const commonStyles = {
62+
display: 'inline-flex',
63+
justifyContent: 'center',
64+
alignItems: 'center',
65+
flex: 'none', // as a flex child
66+
cursor: 'pointer',
67+
fontFamily: 'Inter, sans-serif',
68+
paddingY: 0,
69+
paddingX: 2,
70+
height: '26px', // match with inputs
71+
width: '100%',
72+
fontSize: 2,
73+
fontWeight: 'medium',
74+
lineHeight: 1, // trust the height
75+
border: 'none',
76+
borderRadius: 'small',
77+
transition: 'all ease-in',
78+
transitionDuration: theme => theme.speeds[2],
79+
80+
':focus': {
81+
outline: 'none',
82+
},
83+
':active:not(:disabled)': {
84+
transform: 'scale(0.98)',
85+
},
86+
':disabled': {
87+
opacity: '0.4',
88+
cursor: 'not-allowed',
89+
},
90+
'&[data-loading]': {
91+
opacity: 1,
92+
cursor: 'default',
93+
},
94+
};
95+
96+
const merge = (...objs) =>
97+
objs.reduce(function mergeAll(merged, currentValue = {}) {
98+
return deepmerge(merged, currentValue);
99+
}, {});
100+
60101
export interface ButtonProps
61-
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
102+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
103+
IElementProps {
62104
variant?: 'primary' | 'secondary' | 'link' | 'danger';
105+
loading?: boolean;
63106
}
64107

65-
export const Button = styled(Element).attrs({ as: 'button' })<ButtonProps>(
66-
({ variant = 'primary', ...props }) =>
67-
css(
68-
deepmerge(
69-
// @ts-ignore deepmerge allows functions as values
70-
// it overrides instead of merging, which is what we want
71-
// but it's types don't like it. so we're going to ignore that
72-
// TODO: raise a pull request for deepmerge or pick a different
73-
// library to deep merge objects
74-
variantStyles[variant],
75-
// static styles:
76-
{
77-
display: 'inline-flex',
78-
justifyContent: 'center',
79-
alignItems: 'center',
80-
flex: 'none', // as a flex child
81-
cursor: 'pointer',
82-
fontFamily: 'Inter, sans-serif',
83-
paddingY: 0,
84-
paddingX: 2,
85-
height: '26px', // match with inputs
86-
width: '100%',
87-
fontSize: 2,
88-
fontWeight: 'medium',
89-
lineHeight: 1, // trust the height
90-
border: 'none',
91-
borderRadius: 'small',
92-
transition: 'all ease-in',
93-
transitionDuration: theme => theme.speeds[2],
108+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
109+
function Button({ variant = 'primary', loading, css = {}, ...props }, ref) {
110+
const styles = merge(variantStyles[variant], commonStyles, css);
94111

95-
':focus': {
96-
outline: 'none',
97-
},
98-
':active:not(:disabled)': {
99-
transform: 'scale(0.98)',
100-
},
101-
':disabled': {
102-
opacity: '0.4',
103-
cursor: 'not-allowed',
104-
},
105-
...props.css,
106-
}
107-
)
108-
)
112+
return (
113+
<Element
114+
as="button"
115+
css={styles}
116+
ref={ref}
117+
disabled={props.disabled || loading}
118+
data-loading={loading}
119+
{...props}
120+
>
121+
{loading ? <AnimatingDots /> : props.children}
122+
</Element>
123+
);
124+
}
109125
);
110126

111127
Button.defaultProps = {
112128
type: 'button',
113129
};
130+
131+
/** Animation dots, we use the styled.span syntax
132+
* because keyframes aren't supported in the object syntax
133+
*/
134+
const transition = keyframes({
135+
'0%': { opacity: 0.6 },
136+
'50%': { opacity: 1 },
137+
'100%': { opacity: 0.6 },
138+
});
139+
140+
const Dot = styled.span`
141+
font-size: 18px;
142+
animation: ${transition} 1.5s ease-out infinite;
143+
`;
144+
145+
const AnimatingDots = () => (
146+
<>
147+
<VisuallyHidden>Loading</VisuallyHidden>
148+
<span role="presentation">
149+
<Dot>·</Dot>
150+
<Dot style={{ animationDelay: '200ms' }}>·</Dot>
151+
<Dot style={{ animationDelay: '400ms' }}>·</Dot>
152+
</span>
153+
</>
154+
);

packages/components/src/components/IconButton/iconbutton.stories.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ export default {
99

1010
export const Basic = () => (
1111
<Stack justify="center">
12-
<IconButton label="Filter elements" name="filter" />
12+
<IconButton title="Filter elements" name="filter" />
1313
</Stack>
1414
);
1515

1616
export const Disabled = () => (
17-
<IconButton label="Filter elements disabled" disabled name="filter" />
17+
<IconButton title="Filter elements disabled" disabled name="filter" />
18+
);
19+
20+
export const Sizes = () => (
21+
<Stack justify="center">
22+
<IconButton size={8} title="Filter elements" name="filter" />
23+
<IconButton size={12} title="Filter elements" name="filter" />
24+
<IconButton size={16} title="Filter elements" name="filter" />
25+
</Stack>
1826
);

packages/components/src/components/IconButton/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const IconButton: React.FC<IconButtonProps> = ({
2828
css={deepmerge(
2929
{
3030
width: '26px', // same width as (height of the button)
31-
padding: 0,
31+
paddingX: 0,
3232
borderRadius: '50%',
3333
':hover:not(:disabled)': {
3434
backgroundColor: 'secondaryButton.background',

0 commit comments

Comments
 (0)