Skip to content

Commit e671868

Browse files
authored
Merge pull request #8 from ryanlanciaux/updateCell
Update Cell / Progress Buttons
2 parents 7a29e77 + 1cd0ca2 commit e671868

File tree

10 files changed

+334
-0
lines changed

10 files changed

+334
-0
lines changed

src/Cell/Cell.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.cell-box {
2+
display: flex;
3+
flex-direction: row;
4+
height: 100%;
5+
background: #FFFFFF;
6+
border: 1px solid #EEEFF0;
7+
box-shadow: 0px 2px 4px rgba(41, 49, 58, 0.075);
8+
border-radius: 4px;
9+
font-family: 'Source Code Pro', sans-serif;
10+
}
11+
12+
.cell-box:focus, .cell-box:focus-within {
13+
outline: 0;
14+
background: #FFFFFF;
15+
border: 1px solid #E2E5E7;
16+
17+
box-shadow: 0px 2px 8px rgba(41, 49, 58, 0.15);
18+
border-radius: 4px;
19+
}
20+
21+
.cell-gutter {
22+
padding: 8px 0 0 8px;
23+
height: 100%;
24+
}
25+
26+
.cell-gutter > * {
27+
margin-bottom: 4px;
28+
}
29+
30+
.cell-box.no-gutter {
31+
padding-left: 52px;
32+
}
33+
34+
.cell-body {
35+
padding: 16px;
36+
}

src/Cell/Cell.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { FC, HTMLAttributes, useRef } from 'react';
2+
import classnames from 'classnames';
3+
4+
import './Cell.css';
5+
6+
export interface Props extends HTMLAttributes<HTMLDivElement> {
7+
children?: React.ReactNode | React.ReactNodeArray;
8+
gutter?: React.ReactNode | React.ReactNodeArray;
9+
}
10+
11+
export const Cell: FC<Props> = ({ children, gutter }) => {
12+
const el = useRef(null);
13+
14+
const className = classnames('cell-box', { 'no-gutter': !gutter });
15+
16+
return (
17+
<div tabIndex={-1} className={className} ref={el}>
18+
{gutter}
19+
{children}
20+
</div>
21+
);
22+
};
23+
24+
export const CellBody: FC<HTMLAttributes<HTMLDivElement>> = ({ children }) => (
25+
<div className="cell-body">{children}</div>
26+
);
27+
28+
export const CellGutter: FC<HTMLAttributes<HTMLDivElement>> = ({
29+
children,
30+
}) => <div className="cell-gutter">{children}</div>;

src/Cell/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Cell';

src/CircularButton/CircularButton.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
.circular-button {
3+
position: relative;
4+
--background-color: rgba(0,0,0, 0);
5+
--border-color: #BABFC4;
6+
--radius: 16px;
7+
--fill-color: #757F88;
8+
width: calc(var(--radius) * 2);
9+
height: calc(var(--radius) * 2);
10+
border-radius: var(--radius);
11+
border: 1px solid var(--border-color);
12+
background-color: var(--background-color);
13+
display: flex;
14+
justify-content: center;
15+
align-items: center;
16+
}
17+
18+
.circular-button svg:not(.progress-ring) * {
19+
fill: var(--fill-color);
20+
}
21+
22+
.circular-button svg * {
23+
stroke: var(--fill-color);
24+
}
25+
26+
.circular-button:hover {
27+
--background-color: #EEEFF0;
28+
--border-color:#8F969D;
29+
--fill-color: #29313A;
30+
}
31+
32+
.circular-button:focus, .circular-button.active {
33+
outline: 0;
34+
}
35+
36+
.circular-button:focus, .circular-button.active {
37+
--fill-color: #6100FF;
38+
--border-color: #6100FF;
39+
}
40+
41+
.circular-button.error {
42+
--fill-color: #E53935;
43+
--border-color: #E53935;
44+
}
45+
46+
.circular-button.error:hover {
47+
--fill-color: rgb(233, 82, 79);
48+
--border-color: rgb(233, 82, 79);
49+
}
50+
51+
.circular-button > svg.progress-ring {
52+
display: none;
53+
}
54+
55+
.circular-button.progress {
56+
border: none;
57+
}
58+
.circular-button.progress > .progress-ring {
59+
display: block;
60+
}
61+
62+
.progress-ring {
63+
position: absolute;
64+
top: 0;
65+
left: 0;
66+
}

src/CircularButton/CircularButton.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { FC, HTMLAttributes, useRef } from 'react';
2+
import classnames from 'classnames';
3+
4+
import { ProgressRing } from './ProgressRing';
5+
import './CircularButton.css';
6+
7+
export interface Props extends HTMLAttributes<HTMLButtonElement> {
8+
showPercent?: boolean;
9+
percent?: number;
10+
children?: React.ReactNode | React.ReactNodeArray;
11+
}
12+
13+
export const CircularButton: FC<Props> = ({
14+
showPercent,
15+
percent,
16+
children,
17+
className: additionalClasses,
18+
...props
19+
}) => {
20+
const buttonRef = useRef(null);
21+
const classes = classnames(
22+
'circular-button',
23+
{ progress: showPercent },
24+
additionalClasses
25+
);
26+
27+
return (
28+
<button ref={buttonRef} className={classes} {...props}>
29+
<ProgressRing radius={16} stroke={1} progress={percent || 0} />
30+
{children}
31+
</button>
32+
);
33+
};

src/CircularButton/ProgressRing.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { FC } from 'react';
2+
3+
interface ProgressRingProps {
4+
radius: number;
5+
stroke: number;
6+
progress: number;
7+
}
8+
9+
// Based on https://css-tricks.com/building-progress-ring-quickly/
10+
export const ProgressRing: FC<ProgressRingProps> = ({
11+
radius,
12+
stroke,
13+
progress,
14+
}) => {
15+
const normalizedRadius = radius - stroke;
16+
const circumference = normalizedRadius * 2 * Math.PI;
17+
const strokeDashoffset = circumference - (progress / 100) * circumference;
18+
19+
return (
20+
<svg height={radius * 2} width={radius * 2} className="progress-ring">
21+
<circle
22+
stroke="#000"
23+
fill="transparent"
24+
strokeWidth={stroke}
25+
strokeDasharray={circumference + ' ' + circumference}
26+
style={{ strokeDashoffset }}
27+
r={normalizedRadius}
28+
cx={radius}
29+
cy={radius}
30+
/>
31+
</svg>
32+
);
33+
};

src/CircularButton/icons.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
export const Play = () => {
4+
return (
5+
<svg
6+
width="9"
7+
height="10"
8+
viewBox="0 0 9 10"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M8.13165 4.45728L1.9792 0.612C1.55293 0.34558 1 0.65204 1 1.15472V8.84528C1 9.34796 1.55293 9.65442 1.9792 9.388L8.13165 5.54272C8.53272 5.29205 8.53272 4.70795 8.13165 4.45728Z"
14+
fill="#757F88"
15+
stroke="#757F88"
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
/>
19+
</svg>
20+
);
21+
};
22+
23+
export const More = () => {
24+
return (
25+
<svg
26+
width="16"
27+
height="16"
28+
viewBox="0 0 16 16"
29+
fill="none"
30+
xmlns="http://www.w3.org/2000/svg"
31+
>
32+
<circle cx="8" cy="8" r="1" fill="#757F88" />
33+
<circle cx="4" cy="8" r="1" fill="#757F88" />
34+
<circle cx="12" cy="8" r="1" fill="#757F88" />
35+
</svg>
36+
);
37+
};

src/CircularButton/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './CircularButton';
2+
export * from './icons';

stories/Cell.stories.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { action } from '@storybook/addon-actions';
3+
import { Cell, CellBody, CellGutter, Props } from '../src/Cell';
4+
import { CircularButton, Play, More } from '../src/CircularButton';
5+
6+
export default {
7+
title: 'Cell',
8+
};
9+
10+
export const BaseCell = (props?: Partial<Props>) => {
11+
const [isLoading, setIsLoading] = useState(false);
12+
const [percentComplete, setPercentComplete] = useState(0);
13+
14+
// This is for the Storybook only and
15+
// not production style code
16+
useEffect(() => {
17+
if (isLoading) {
18+
const interval = window.setInterval(() => {
19+
if (percentComplete === 99) {
20+
setIsLoading(false);
21+
window.clearInterval(interval);
22+
setPercentComplete(0);
23+
return;
24+
}
25+
26+
setPercentComplete(previous => previous + 1);
27+
}, 60);
28+
29+
return () => {
30+
clearInterval(interval);
31+
};
32+
}
33+
}, [isLoading, percentComplete]);
34+
35+
return (
36+
<Cell
37+
gutter={
38+
<CellGutter>
39+
<CircularButton
40+
showPercent={isLoading}
41+
onClick={() => {
42+
setIsLoading(p => !p);
43+
}}
44+
percent={percentComplete}
45+
>
46+
<Play />
47+
</CircularButton>
48+
<CircularButton>
49+
<More />
50+
</CircularButton>
51+
</CellGutter>
52+
}
53+
>
54+
<CellBody>
55+
Enim reprehenderit in nulla laboris duis laboris anim tempor cupidatat
56+
excepteur. Nostrud amet nostrud excepteur adipisicing nulla non mollit
57+
irure eiusmod magna occaecat. Adipisicing voluptate ex voluptate irure
58+
deserunt non excepteur sit voluptate excepteur fugiat.
59+
</CellBody>
60+
</Cell>
61+
);
62+
};

stories/CircularButton.stories.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { action } from '@storybook/addon-actions';
3+
import { CircularButton, Props, Play } from '../src/CircularButton';
4+
5+
export default {
6+
title: 'CircularButton',
7+
};
8+
9+
export const Standard = (props?: Partial<Props>) => (
10+
<CircularButton onClick={action('Clicked')}>
11+
<Play />
12+
</CircularButton>
13+
);
14+
15+
export const Progress = (props?: Partial<Props>) => {
16+
const [complete, setComplete] = useState(0);
17+
18+
useEffect(() => {
19+
window.setInterval(() => {
20+
setComplete(previous => (previous + 1 === 100 ? 0 : previous + 1));
21+
}, 60);
22+
}, []);
23+
24+
return (
25+
<CircularButton
26+
onClick={action('Clicked')}
27+
className="active"
28+
showPercent={true}
29+
percent={complete}
30+
>
31+
<Play />
32+
</CircularButton>
33+
);
34+
};

0 commit comments

Comments
 (0)