Skip to content

Commit 44b6b42

Browse files
authored
Checkbox 컴포넌트 추가 (#72)
* Checkbox 추가 * Styled-component를 위한 interface 앞에 Styled prefix 붙이기. * 스타일 코드 변경. * export 방식 변경. * 테스트 코드 추가. * import type 변경, enum 별도 파일로 분리. * import lodash > lodash-es
1 parent cd66de2 commit 44b6b42

File tree

10 files changed

+416
-0
lines changed

10 files changed

+416
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,6 @@ build/
190190

191191
# Story book
192192
storybook-static/
193+
194+
# WebStorm
195+
.idea
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
enum CheckType {
2+
False,
3+
True,
4+
Partial,
5+
}
6+
7+
export default CheckType
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/* External dependencies */
2+
import React, { useRef, useState, useCallback } from 'react'
3+
import { action } from '@storybook/addon-actions'
4+
import { boolean, select, withKnobs } from '@storybook/addon-knobs'
5+
import styled from 'styled-components'
6+
import { random } from 'lodash-es'
7+
8+
/* Internal dependencies */
9+
import { LightTheme, ThemeProvider } from '../../styling/Theme'
10+
import Checkbox from './Checkbox'
11+
import CheckType from './CheckType'
12+
13+
const checkOptions = {
14+
False: CheckType.False,
15+
True: CheckType.True,
16+
Partial: CheckType.Partial,
17+
}
18+
19+
function randomRGB() {
20+
return `rgb(${random(255)}, ${random(255)}, ${random(255)})`
21+
}
22+
23+
const Wrapper = styled.div`
24+
display: flex;
25+
flex-direction: column;
26+
width: fit-content;
27+
max-width: 200px;
28+
`
29+
30+
export default {
31+
title: 'Checkbox',
32+
decorators: [withKnobs],
33+
}
34+
35+
export const Primary = () => (
36+
<ThemeProvider theme={LightTheme}>
37+
<Checkbox
38+
disabled={boolean('disabled', false)}
39+
checked={select('checked', checkOptions, CheckType.True)}
40+
onClick={action('onClick')}
41+
>
42+
Check Me!
43+
</Checkbox>
44+
</ThemeProvider>
45+
)
46+
47+
export const Controlled = () => {
48+
const [checked, setChecked] = useState<CheckType>(CheckType.True)
49+
const handleClick = useCallback(() => {
50+
switch (checked) {
51+
case CheckType.False:
52+
setChecked(CheckType.True)
53+
break
54+
case CheckType.True:
55+
setChecked(CheckType.Partial)
56+
break
57+
case CheckType.Partial:
58+
setChecked(CheckType.False)
59+
break
60+
}
61+
}, [
62+
checked,
63+
])
64+
return (
65+
<ThemeProvider theme={LightTheme}>
66+
<Checkbox
67+
disabled={boolean('disabled', false)}
68+
checked={checked}
69+
onClick={handleClick}
70+
>
71+
Check Me!
72+
</Checkbox>
73+
</ThemeProvider>
74+
)
75+
}
76+
77+
export const WithRef = () => {
78+
const checkboxRef = useRef<HTMLDivElement | null>(null)
79+
const handleClickButton = useCallback(() => {
80+
checkboxRef.current?.setAttribute('style', `
81+
background-color: ${randomRGB()};
82+
color: ${randomRGB()};
83+
transition: background-color .15s ease-in-out;
84+
`)
85+
}, [])
86+
return (
87+
<ThemeProvider theme={LightTheme}>
88+
<Wrapper>
89+
<Checkbox
90+
ref={checkboxRef}
91+
disabled={boolean('disabled', false)}
92+
checked={select('checked', checkOptions, CheckType.True)}
93+
onClick={action('onClick')}
94+
>
95+
Check Me!
96+
</Checkbox>
97+
<button
98+
type="button"
99+
onClick={handleClickButton}
100+
>
101+
Change Color!
102+
</button>
103+
</Wrapper>
104+
</ThemeProvider>
105+
)
106+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* External dependencies */
2+
import styled from 'styled-components'
3+
4+
/* Internal dependencies */
5+
import { absoluteCenter } from '../../styling/Mixins'
6+
import Palette from '../../styling/Palette'
7+
import { StyledWrapperProps, StyledCheckerProps, StyledContentProps } from './Checkbox.types'
8+
import CheckType from './CheckType'
9+
10+
const CHECKER_BOX_SIZE = 18
11+
const CHECKER_ICON_THICKNESS = 2
12+
const CHECKER_BORDER_THICKNESS = 2
13+
14+
const TRANSITION_DURATION = '.15s'
15+
const TRANSITION_FUNCTION = 'ease-in-out'
16+
17+
export const Wrapper = styled.div<StyledWrapperProps>`
18+
display: inline-flex;
19+
align-items: center;
20+
cursor: pointer;
21+
22+
${props => (props.disabled
23+
? 'cursor: not-allowed;'
24+
: ''
25+
)}
26+
`
27+
28+
const checkerBase = (props: StyledCheckerProps) => `
29+
&::after {
30+
${absoluteCenter`translateY(-13%) rotate(42deg)`}
31+
width: 4px;
32+
height: 9px;
33+
border-right: solid ${CHECKER_ICON_THICKNESS}px transparent;
34+
border-bottom: solid ${CHECKER_ICON_THICKNESS}px transparent;
35+
border-color: ${Palette.white};
36+
content: '';
37+
transition: border-color ${TRANSITION_DURATION} ${TRANSITION_FUNCTION};
38+
}
39+
40+
${(props.checkStatus === CheckType.True || props.checkStatus === CheckType.Partial) ? `
41+
background-color: ${Palette.green400};
42+
border-color: transparent;
43+
44+
${!props.disabled ? `
45+
&:hover {
46+
background-color: ${Palette.green500};
47+
}
48+
` : ''}
49+
` : ''}
50+
`
51+
52+
const partialChecked = (props: StyledCheckerProps) => ((props.checkStatus === CheckType.Partial) ? `
53+
&::after {
54+
${absoluteCenter`translateY(-36%) rotate(0)`}
55+
width: 10px;
56+
border-right: none;
57+
border-bottom: solid 2px ${Palette.white};
58+
}
59+
` : '')
60+
61+
const disabled = (props: StyledCheckerProps) => ((props.disabled) ? `
62+
background-color: ${props.theme?.colors?.disabled3};
63+
64+
${(props.checkStatus === CheckType.False) ? `
65+
&::after {
66+
border-color: transparent;
67+
}
68+
` : `
69+
&::after {
70+
border-color: ${Palette.grey500};
71+
}
72+
`}
73+
` : '')
74+
75+
export const Checker = styled.div<StyledCheckerProps>`
76+
position: relative;
77+
display: flex;
78+
align-items: center;
79+
justify-content: center;
80+
box-sizing: border-box !important;
81+
width: ${CHECKER_BOX_SIZE}px;
82+
height: ${CHECKER_BOX_SIZE}px;
83+
min-width: ${CHECKER_BOX_SIZE}px;
84+
min-height: ${CHECKER_BOX_SIZE}px;
85+
font-size: 10px;
86+
color: transparent;
87+
background-color: ${Palette.white};
88+
border: ${CHECKER_BORDER_THICKNESS}px solid ${props => props.theme?.colors?.border3};
89+
border-radius: 4px;
90+
transition:
91+
${`color ${TRANSITION_DURATION} ${TRANSITION_FUNCTION}`},
92+
${`background ${TRANSITION_DURATION} ${TRANSITION_FUNCTION}`};
93+
94+
${props => (!props.disabled ? `
95+
&:hover {
96+
&::after {
97+
border-color: ${Palette.grey200};
98+
}
99+
}
100+
` : '')}
101+
102+
${checkerBase}
103+
104+
${partialChecked}
105+
106+
${disabled}
107+
`
108+
109+
export const Content = styled.div<StyledContentProps>`
110+
box-sizing: border-box;
111+
padding: ${CHECKER_BORDER_THICKNESS}px 0;
112+
margin-left: 2px;
113+
user-select: none;
114+
`
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import { render } from '@testing-library/react'
4+
5+
/* Internal dependencies */
6+
import { ThemeProvider, LightTheme } from '../../styling/Theme'
7+
import Palette from '../../styling/Palette'
8+
import { Light as LightColors } from '../../styling/Colors'
9+
import Checkbox, { CHECKBOX_TEST_ID, CHECKBOX_CHECKER_TEST_ID } from './Checkbox'
10+
import CheckboxProps from './Checkbox.types'
11+
import CheckType from './CheckType'
12+
13+
describe('Checkbox test >', () => {
14+
let props: CheckboxProps
15+
16+
beforeEach(() => {
17+
props = {
18+
contentClassName: undefined,
19+
disabled: false,
20+
checked: CheckType.False,
21+
}
22+
})
23+
24+
const renderComponent = (optionProps?: CheckboxProps) => render(
25+
<ThemeProvider theme={LightTheme}>
26+
<Checkbox {...props} {...optionProps} />
27+
</ThemeProvider>,
28+
)
29+
30+
it('Checkbox has default style', () => {
31+
const { getByTestId } = renderComponent()
32+
33+
const renderedCheckbox = getByTestId(CHECKBOX_TEST_ID)
34+
35+
expect(renderedCheckbox).toHaveStyle('display: inline-flex;')
36+
expect(renderedCheckbox).toHaveStyle('align-items: center;')
37+
expect(renderedCheckbox).toHaveStyle('cursor: pointer;')
38+
})
39+
40+
it('Checker of Checkbox has default style', () => {
41+
const { getByTestId } = renderComponent()
42+
43+
const renderedCheckboxChecker = getByTestId(CHECKBOX_CHECKER_TEST_ID)
44+
45+
expect(renderedCheckboxChecker).toHaveStyle('position: relative;')
46+
expect(renderedCheckboxChecker).toHaveStyle('display: flex;')
47+
expect(renderedCheckboxChecker).toHaveStyle('align-items: center;')
48+
expect(renderedCheckboxChecker).toHaveStyle('justify-content: center;')
49+
expect(renderedCheckboxChecker).toHaveStyle('box-sizing: border-box;')
50+
})
51+
52+
it('Checker of Checkbox has green background when check status is falsy', () => {
53+
const { getByTestId } = renderComponent()
54+
55+
const renderedCheckboxChecker = getByTestId(CHECKBOX_CHECKER_TEST_ID)
56+
57+
expect(renderedCheckboxChecker).toHaveStyle(`background-color: ${Palette.white};`)
58+
expect(renderedCheckboxChecker).toHaveStyle(`border-color: ${LightColors.border3};`)
59+
})
60+
61+
it('Checker of Checkbox has green background when check status is truthy', () => {
62+
const { getByTestId } = renderComponent({ checked: true })
63+
64+
const renderedCheckboxChecker = getByTestId(CHECKBOX_CHECKER_TEST_ID)
65+
66+
expect(renderedCheckboxChecker).toHaveStyle(`background-color: ${Palette.green400};`)
67+
expect(renderedCheckboxChecker).toHaveStyle('border-color: transparent;')
68+
})
69+
70+
it('Checker of Checkbox has grey background when disabled', () => {
71+
const { getByTestId } = renderComponent({ disabled: true })
72+
73+
const renderedCheckboxChecker = getByTestId(CHECKBOX_CHECKER_TEST_ID)
74+
75+
expect(renderedCheckboxChecker).toHaveStyle(`background-color: ${LightColors.disabled3};`)
76+
})
77+
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* External dependencies */
2+
import React, {
3+
Ref,
4+
forwardRef,
5+
useMemo,
6+
} from 'react'
7+
import { values, isBoolean, isEmpty, includes, noop } from 'lodash-es'
8+
9+
/* Internal dependencies */
10+
import type CheckboxProps from './Checkbox.types'
11+
import { Wrapper, Checker, Content } from './Checkbox.styled'
12+
import CheckType from './CheckType'
13+
14+
export const CHECKBOX_TEST_ID = 'ch-design-system-checkbox'
15+
export const CHECKBOX_CHECKER_TEST_ID = 'ch-design-system-checkbox-checker'
16+
17+
const checkTypeValues = values(CheckType)
18+
19+
function Checkbox(
20+
{
21+
as,
22+
testId = CHECKBOX_TEST_ID,
23+
checkerTestId = CHECKBOX_CHECKER_TEST_ID,
24+
className,
25+
contentClassName,
26+
disabled = false,
27+
checked = false,
28+
children,
29+
onClick,
30+
}: CheckboxProps,
31+
forwardedRef: Ref<any>,
32+
) {
33+
const checkStatus = useMemo(() => {
34+
if (isBoolean(checked)) { return (checked) ? CheckType.True : CheckType.False }
35+
if (includes(checkTypeValues, checked)) { return checked }
36+
return CheckType.False
37+
}, [
38+
checked,
39+
])
40+
41+
return (
42+
<Wrapper
43+
ref={forwardedRef}
44+
as={as}
45+
className={className}
46+
disabled={disabled}
47+
onClick={disabled ? noop : onClick}
48+
data-testid={testId}
49+
>
50+
<Checker
51+
disabled={disabled}
52+
checkStatus={checkStatus}
53+
data-testid={checkerTestId}
54+
/>
55+
56+
{ !isEmpty(children) ? (
57+
<Content className={contentClassName}>
58+
{ children }
59+
</Content>
60+
) : null }
61+
</Wrapper>
62+
)
63+
}
64+
65+
export default forwardRef(Checkbox)

0 commit comments

Comments
 (0)