Skip to content

Commit 8c77b7a

Browse files
committed
feat(counter): add Counter component
#127
1 parent 087e235 commit 8c77b7a

File tree

7 files changed

+374
-7
lines changed

7 files changed

+374
-7
lines changed

src/Counter/Counter.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import propTypes from 'prop-types';
3+
import styled from 'styled-components';
4+
5+
import { createWellBorderStyles } from '../common';
6+
import Digit from './Digit';
7+
8+
const CounterWrapper = styled.div`
9+
${createWellBorderStyles(true)}
10+
display: inline-flex;
11+
background: #000000;
12+
`;
13+
14+
const pixelSizes = {
15+
sm: 1,
16+
md: 2,
17+
lg: 3,
18+
xl: 4
19+
};
20+
21+
const Counter = React.forwardRef(function Counter(props, ref) {
22+
const { value, minLength, size, ...otherProps } = props;
23+
let stringValue = value.toString();
24+
if (minLength && minLength > stringValue.length) {
25+
stringValue =
26+
Array(minLength - stringValue.length)
27+
.fill('0')
28+
.join('') + stringValue;
29+
}
30+
return (
31+
<CounterWrapper ref={ref} {...otherProps}>
32+
{stringValue.split('').map((digit, i) => (
33+
<Digit digit={digit} pixelSize={pixelSizes[size]} key={i} />
34+
))}
35+
</CounterWrapper>
36+
);
37+
});
38+
39+
Counter.defaultProps = {
40+
minLength: 3,
41+
size: 'md',
42+
value: 0
43+
};
44+
45+
Counter.propTypes = {
46+
minLength: propTypes.number,
47+
size: propTypes.oneOf(['sm', 'md', 'lg']),
48+
value: propTypes.number
49+
};
50+
51+
export default Counter;

src/Counter/Counter.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
name: Bar
3+
menu: Components
4+
---
5+
6+
import { Playground, Props } from 'docz';
7+
8+
import Counter from '../Counter/Counter'
9+
10+
# Counter
11+
12+
## Usage
13+
14+
<Playground>
15+
<Counter value={4017} minLength={7} size='lg' />
16+
</Playground>
17+
18+
## API
19+
20+
### Import
21+
22+
### Props
23+
24+
<Props of={Counter} />

src/Counter/Counter.spec.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import { renderWithTheme } from '../../test/utils';
3+
4+
import Counter from './Counter';
5+
6+
describe('<Counter />', () => {
7+
it('should render', () => {
8+
const { container } = renderWithTheme(<Counter />);
9+
const counter = container.firstChild;
10+
11+
expect(counter).toBeInTheDocument();
12+
});
13+
14+
it('should handle custom style', () => {
15+
const { container } = renderWithTheme(
16+
<Counter style={{ backgroundColor: 'papayawhip' }} />
17+
);
18+
const counter = container.firstChild;
19+
20+
expect(counter).toHaveAttribute('style', 'background-color: papayawhip;');
21+
});
22+
23+
it('should handle custom props', () => {
24+
const customProps = { title: 'potatoe' };
25+
const { container } = renderWithTheme(<Counter {...customProps} />);
26+
const counter = container.firstChild;
27+
28+
expect(counter).toHaveAttribute('title', 'potatoe');
29+
});
30+
31+
describe('prop: minLength', () => {
32+
it('renders correct number of digits', () => {
33+
const { container } = renderWithTheme(
34+
<Counter value={32} minLength={7} />
35+
);
36+
const counter = container.firstChild;
37+
38+
expect(counter.childElementCount).toBe(7);
39+
});
40+
41+
it('value length takes priority if bigger than minLength', () => {
42+
const { container } = renderWithTheme(
43+
<Counter value={1234} minLength={2} />
44+
);
45+
const counter = container.firstChild;
46+
47+
expect(counter.childElementCount).toBe(4);
48+
});
49+
});
50+
});

src/Counter/Counter.stories.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { useState } from 'react';
2+
import styled from 'styled-components';
3+
4+
import { Counter, Panel, Button } from 'react95';
5+
6+
const Wrapper = styled.div`
7+
padding: 5rem;
8+
background: teal;
9+
.counter-wrapper {
10+
display: flex;
11+
margin-top: 1rem;
12+
}
13+
.counter-wrapper button {
14+
margin-left: 0.5rem;
15+
height: 51px;
16+
}
17+
.wrapper {
18+
padding: 1rem;
19+
}
20+
`;
21+
22+
export default {
23+
title: 'Counter',
24+
component: Counter,
25+
decorators: [story => <Wrapper>{story()}</Wrapper>]
26+
};
27+
28+
export const Default = () => {
29+
const [count, setCount] = useState(13);
30+
const handleClick = () => setCount(count + 1);
31+
return (
32+
<Panel className='wrapper'>
33+
<Counter value={123456789} minLength={11} size='lg' />
34+
35+
<div className='counter-wrapper'>
36+
<Counter value={count} minLength={3} />
37+
<Button onClick={handleClick}>Click!</Button>
38+
</div>
39+
</Panel>
40+
);
41+
};
42+
43+
Default.story = {
44+
name: 'default'
45+
};

src/Counter/Digit.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React from 'react';
2+
import propTypes from 'prop-types';
3+
import styled, { css } from 'styled-components';
4+
5+
import { createHatchedBackground } from '../common';
6+
7+
const DigitWrapper = styled.div`
8+
position: relative;
9+
--react95-digit-primary-color: #ff0102;
10+
--react95-digit-secondary-color: #740201;
11+
--react95-digit-bg-color: #000000;
12+
13+
${({ pixelSize }) => css`
14+
width: ${11 * pixelSize}px;
15+
height: ${21 * pixelSize}px;
16+
margin: ${pixelSize}px;
17+
18+
span,
19+
span:before,
20+
span:after {
21+
box-sizing: border-box;
22+
display: inline-block;
23+
position: absolute;
24+
}
25+
span.active,
26+
span.active:before,
27+
span.active:after {
28+
background: var(--react95-digit-primary-color);
29+
}
30+
span:not(.active),
31+
span:not(.active):before,
32+
span:not(.active):after {
33+
${createHatchedBackground({
34+
mainColor: 'var(--react95-digit-bg-color)',
35+
secondaryColor: 'var(--react95-digit-secondary-color)',
36+
pixelSize
37+
})}
38+
}
39+
40+
span.horizontal,
41+
span.horizontal:before,
42+
span.horizontal:after {
43+
height: ${pixelSize}px;
44+
border-left: ${pixelSize}px solid var(--react95-digit-bg-color);
45+
border-right: ${pixelSize}px solid var(--react95-digit-bg-color);
46+
}
47+
span.horizontal.active,
48+
span.horizontal.active:before,
49+
span.horizontal.active:after {
50+
height: ${pixelSize}px;
51+
border-left: ${pixelSize}px solid var(--react95-digit-primary-color);
52+
border-right: ${pixelSize}px solid var(--react95-digit-primary-color);
53+
}
54+
span.horizontal {
55+
left: ${pixelSize}px;
56+
width: ${9 * pixelSize}px;
57+
}
58+
span.horizontal:before {
59+
content: '';
60+
width: 100%;
61+
top: ${pixelSize}px;
62+
left: ${0}px;
63+
}
64+
span.horizontal:after {
65+
content: '';
66+
width: calc(100% - ${pixelSize * 2}px);
67+
top: ${2 * pixelSize}px;
68+
left: ${pixelSize}px;
69+
}
70+
span.horizontal.top {
71+
top: 0;
72+
}
73+
span.horizontal.bottom {
74+
bottom: 0;
75+
transform: rotateX(180deg);
76+
}
77+
78+
span.center,
79+
span.center:before,
80+
span.center:after {
81+
height: ${pixelSize}px;
82+
border-left: ${pixelSize}px solid var(--react95-digit-bg-color);
83+
border-right: ${pixelSize}px solid var(--react95-digit-bg-color);
84+
}
85+
span.center.active,
86+
span.center.active:before,
87+
span.center.active:after {
88+
border-left: ${pixelSize}px solid var(--react95-digit-primary-color);
89+
border-right: ${pixelSize}px solid var(--react95-digit-primary-color);
90+
}
91+
span.center {
92+
top: 50%;
93+
transform: translateY(-50%);
94+
left: ${pixelSize}px;
95+
width: ${9 * pixelSize}px;
96+
}
97+
span.center:before,
98+
span.center:after {
99+
content: '';
100+
width: 100%;
101+
}
102+
span.center:before {
103+
top: ${pixelSize}px;
104+
}
105+
span.center:after {
106+
bottom: ${pixelSize}px;
107+
}
108+
109+
span.vertical,
110+
span.vertical:before,
111+
span.vertical:after {
112+
width: ${pixelSize}px;
113+
border-top: ${pixelSize}px solid var(--react95-digit-bg-color);
114+
border-bottom: ${pixelSize}px solid var(--react95-digit-bg-color);
115+
}
116+
span.vertical {
117+
height: ${11 * pixelSize}px;
118+
}
119+
span.vertical.left {
120+
left: 0;
121+
}
122+
span.vertical.right {
123+
right: 0;
124+
transform: rotateY(180deg);
125+
}
126+
span.vertical.top {
127+
top: 0px;
128+
}
129+
span.vertical.bottom {
130+
bottom: 0px;
131+
}
132+
span.vertical:before {
133+
content: '';
134+
height: 100%;
135+
top: ${0}px;
136+
left: ${pixelSize}px;
137+
}
138+
span.vertical:after {
139+
content: '';
140+
height: calc(100% - ${pixelSize * 2}px);
141+
top: ${pixelSize}px;
142+
left: ${pixelSize * 2}px;
143+
}
144+
`}
145+
`;
146+
147+
const segments = [
148+
'horizontal top',
149+
'center',
150+
'horizontal bottom',
151+
'vertical top left',
152+
'vertical top right',
153+
'vertical bottom left',
154+
'vertical bottom right'
155+
];
156+
157+
const digitActiveSegments = [
158+
[1, 0, 1, 1, 1, 1, 1], // 0
159+
[0, 0, 0, 0, 1, 0, 1], // 1
160+
[1, 1, 1, 0, 1, 1, 0], // 2
161+
[1, 1, 1, 0, 1, 0, 1], // 3
162+
[0, 1, 0, 1, 1, 0, 1], // 4
163+
[1, 1, 1, 1, 0, 0, 1], // 5
164+
[1, 1, 1, 1, 0, 1, 1], // 6
165+
[1, 0, 0, 0, 1, 0, 1], // 7
166+
[1, 1, 1, 1, 1, 1, 1], // 8
167+
[1, 1, 1, 1, 1, 0, 1] // 9
168+
];
169+
170+
const Digit = ({ digit, pixelSize, ...otherProps }) => {
171+
const segmentClasses = digitActiveSegments[digit].map((isActive, i) =>
172+
isActive ? `${segments[i]} active` : segments[i]
173+
);
174+
return (
175+
<DigitWrapper pixelSize={pixelSize} {...otherProps}>
176+
{segmentClasses.map((className, i) => (
177+
<span className={className} key={i} />
178+
))}
179+
</DigitWrapper>
180+
);
181+
};
182+
183+
Digit.defaultProps = {
184+
pixelSize: 2,
185+
digit: 0
186+
};
187+
188+
Digit.propTypes = {
189+
pixelSize: propTypes.number,
190+
digit: propTypes.oneOfType(propTypes.number, propTypes.string)
191+
};
192+
193+
export default Digit;

0 commit comments

Comments
 (0)