Skip to content

Commit cd66de2

Browse files
authored
Radio 컴포넌트 (#47)
* Radio 베이스 * Radio style 수정 * 스타일 수정 및 prop 재조정 * 라디오 전체 hover 인식하도록 변경 및 checked 계산 로직 수정 * 테스트 코드 추가 * forwardRef 추가 * Styled interface 위치 수정 * Han, Lloyd 리뷰 반영 * lodash -> lodash-es 로 변경 (Han 리뷰 반영)
1 parent 35386e8 commit cd66de2

File tree

10 files changed

+329
-5
lines changed

10 files changed

+329
-5
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* External dependencies */
2+
import React, { useState } from 'react'
3+
import { withKnobs, text } from '@storybook/addon-knobs'
4+
5+
/* Internal dependencies */
6+
import { Text } from '../Text'
7+
import Typography from '../../styling/Typography'
8+
import { ThemeProvider, LightTheme } from '../../styling/Theme'
9+
import Radio from './Radio'
10+
11+
export default {
12+
title: 'Radio',
13+
decorators: [withKnobs],
14+
}
15+
16+
export const Primary = () => {
17+
const value = 1
18+
19+
return (
20+
<ThemeProvider theme={LightTheme}>
21+
<div style={{ margin: 10 }}>
22+
<Radio
23+
watchingValue={value}
24+
value={value}
25+
>
26+
<Text>
27+
{ text('label', 'hello!') }
28+
</Text>
29+
</Radio>
30+
</div>
31+
</ThemeProvider>
32+
)
33+
}
34+
35+
export const Disabled = () => (
36+
<ThemeProvider theme={LightTheme}>
37+
<Radio disabled>
38+
<Text>
39+
{ text('label', 'hello, world!') }
40+
</Text>
41+
</Radio>
42+
</ThemeProvider>
43+
)
44+
45+
export const CheckedDisabled = () => {
46+
const value = 1
47+
48+
return (
49+
<ThemeProvider theme={LightTheme}>
50+
<Radio
51+
value={value}
52+
watchingValue={value}
53+
disabled
54+
>
55+
<Text>
56+
{ text('label', 'hello, world!') }
57+
</Text>
58+
</Radio>
59+
</ThemeProvider>
60+
)
61+
}
62+
63+
export const MultiRadio = () => {
64+
const [selected, setSelected] = useState(1)
65+
66+
const options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
67+
68+
return (
69+
<ThemeProvider theme={LightTheme}>
70+
<Text typo={Typography.Size24}>
71+
selected Option: { selected }
72+
</Text>
73+
74+
{
75+
options.map(value => (
76+
<Radio
77+
key={value}
78+
value={value}
79+
watchingValue={selected}
80+
onClick={v => setSelected(v)}
81+
disabled={value >= 8}
82+
>
83+
<Text>
84+
{ value }st option
85+
</Text>
86+
</Radio>
87+
))
88+
}
89+
</ThemeProvider>
90+
)
91+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* eslint-disable @typescript-eslint/indent, consistent-return */
2+
/* External dependencies */
3+
import styled, { css } from 'styled-components'
4+
5+
/* Internal dependencies */
6+
import RadioProps, { StyledRadioHandleProps } from './Radio.types'
7+
8+
export const StyledRadioWrapper = styled.div<RadioProps>`
9+
display: flex;
10+
align-items: center;
11+
cursor:
12+
${props => {
13+
if (props.disabled) { return 'auto' }
14+
return 'pointer'
15+
}};
16+
`
17+
18+
const StyledRadioHandleDot = css<StyledRadioHandleProps>`
19+
position: absolute;
20+
top: 50%;
21+
left: 50%;
22+
transform: translate(-50%, -50%);
23+
width: 8px;
24+
height: 8px;
25+
content: '';
26+
background-color:
27+
${props => {
28+
if (!props.disabled && !props.checked && props.hovered) { return props.theme?.colors?.handle2 }
29+
if (props.checked && props.disabled) { return props.theme?.colors?.handle5 }
30+
if (props.checked) { return props.theme?.colors?.handle1 }
31+
return 'transparent'
32+
}};
33+
border-radius: 50%;
34+
`
35+
36+
export const StyledRadioHandle = styled.div<RadioProps & StyledRadioHandleProps>`
37+
box-sizing: border-box;
38+
position: relative;
39+
width: 18px;
40+
height: 18px;
41+
margin-right: 9px;
42+
border-radius: 50%;
43+
border:
44+
${props => {
45+
if (props.checked) { return 'none' }
46+
return `2px solid ${props.theme?.colors?.border3}`
47+
}};
48+
background-color:
49+
${props => {
50+
if (props.disabled) { return props.theme?.colors?.background3 }
51+
if (props.checked) { return props.theme?.colors?.success1 }
52+
return props.theme?.colors?.background0
53+
}};
54+
55+
&::after {
56+
${StyledRadioHandleDot};
57+
}
58+
`
59+
/* eslint-enable @typescript-eslint/indent, consistent-return */
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import { render } from '@testing-library/react'
4+
5+
/* Internal dependencies */
6+
import Colors from '../../styling/Colors'
7+
import { ThemeProvider, LightTheme } from '../../styling/Theme'
8+
import Radio, { RADIO_TEST_ID } from './Radio'
9+
import RadioProps from './Radio.types'
10+
11+
const value = 'text'
12+
13+
describe('Radio test >', () => {
14+
let props: RadioProps
15+
16+
beforeEach(() => {
17+
props = {
18+
disabled: false,
19+
value,
20+
watchingValue: '',
21+
}
22+
})
23+
24+
const renderRadio = (optionProps?: RadioProps) => render(
25+
<ThemeProvider theme={LightTheme}>
26+
<Radio {...props} {...optionProps}/>
27+
</ThemeProvider>,
28+
)
29+
30+
it('RadioInput has default style', () => {
31+
const { getByTestId } = renderRadio()
32+
33+
const renderedRadio = getByTestId(RADIO_TEST_ID)
34+
35+
expect(renderedRadio).toHaveStyle('width: 18px;')
36+
expect(renderedRadio).toHaveStyle('height: 18px;')
37+
expect(renderedRadio).toHaveStyle('border-radius: 50%;')
38+
expect(renderedRadio).toHaveStyle(`background-color: ${Colors.Light.background0};`)
39+
})
40+
41+
it('RadioInput has success background, and no border when clicked', () => {
42+
const { getByTestId } = renderRadio({ watchingValue: value })
43+
44+
const renderedRadio = getByTestId(RADIO_TEST_ID)
45+
46+
expect(renderedRadio).toHaveStyle(`background-color: ${Colors.Light.success1};`)
47+
expect(renderedRadio).toHaveStyle('border: none;')
48+
})
49+
50+
it('RadioInput has disable background when disabled', () => {
51+
const { getByTestId } = renderRadio({ disabled: true })
52+
53+
const renderedRadio = getByTestId(RADIO_TEST_ID)
54+
55+
expect(renderedRadio).toHaveStyle(`background-color: ${Colors.Light.background3};`)
56+
})
57+
})

src/components/Radio/Radio.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* External dependencies */
2+
import React, {
3+
useCallback,
4+
useState,
5+
useMemo,
6+
MouseEvent,
7+
forwardRef,
8+
} from 'react'
9+
import { isNil } from 'lodash-es'
10+
11+
/* Internal dependencies */
12+
import {
13+
StyledRadioWrapper,
14+
StyledRadioHandle,
15+
} from './Radio.styled'
16+
import RadioProps from './Radio.types'
17+
18+
export const RADIO_TEST_ID = 'ch-design-system-radio'
19+
20+
function Radio(
21+
{
22+
as,
23+
testId = RADIO_TEST_ID,
24+
className,
25+
style,
26+
dotClassName,
27+
watchingValue,
28+
value,
29+
onClick,
30+
disabled = false,
31+
children,
32+
...otherProps
33+
}: RadioProps,
34+
forwardedRef: React.Ref<HTMLElement>,
35+
) {
36+
const [hovered, setHovered] = useState(false)
37+
38+
const handleClick = useCallback((e: MouseEvent) => {
39+
if (!disabled && onClick) {
40+
onClick(value, e)
41+
}
42+
}, [onClick, disabled, value])
43+
44+
const handleMouseOver = useCallback(() => setHovered(true), [])
45+
46+
const handleMouseLeave = useCallback(() => setHovered(false), [])
47+
48+
const checked = useMemo(() => {
49+
if (isNil(watchingValue) || isNil(value)) { return false }
50+
return watchingValue === value
51+
}, [watchingValue, value])
52+
53+
return (
54+
<StyledRadioWrapper
55+
ref={forwardedRef}
56+
className={className}
57+
style={style}
58+
disabled={disabled}
59+
onClick={handleClick}
60+
onMouseEnter={handleMouseOver}
61+
onMouseLeave={handleMouseLeave}
62+
{...otherProps}
63+
>
64+
<StyledRadioHandle
65+
as={as}
66+
data-testid={testId}
67+
className={dotClassName}
68+
checked={checked}
69+
disabled={disabled}
70+
hovered={hovered}
71+
/>
72+
{ children }
73+
</StyledRadioWrapper>
74+
)
75+
}
76+
77+
export default forwardRef(Radio)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* External dependencies */
2+
import { MouseEvent } from 'react'
3+
4+
/* Internal dependencies */
5+
import { ChildrenComponentProps } from '../../types/ComponentProps'
6+
7+
export default interface RadioProps extends ChildrenComponentProps {
8+
dotClassName?: string
9+
watchingValue?: any
10+
disabled?: boolean
11+
value?: any
12+
onClick?: (value: any, e: MouseEvent) => void
13+
}
14+
15+
export interface StyledRadioHandleProps {
16+
checked: boolean
17+
disabled: boolean
18+
hovered: boolean
19+
}

src/components/Radio/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Radio from './Radio'
2+
import type RadioProps from './Radio.types'
3+
4+
export type {
5+
RadioProps,
6+
}
7+
8+
export {
9+
Radio,
10+
}

src/components/Text/Text.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,4 @@ function Text({
3333
)
3434
}
3535

36-
Text.displayName = 'Text'
37-
3836
export default Text

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './components/Button'
33
export * from './components/Icon'
44
export * from './components/Switch'
55
export * from './components/Text'
6+
export * from './components/Radio'
67

78
/* Layout */
89
export * from './layout/GNB'

src/styling/Colors.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export interface Colors {
88
background3?: string
99
background2?: string
1010
background1?: string
11+
background0?: string
12+
13+
// Borders
14+
border3?: string
15+
border2?: string
16+
border1?: string
1117

1218
// Success
1319
success1?: string
@@ -18,6 +24,8 @@ export interface Colors {
1824
default1Hover?: string
1925

2026
// Handle
27+
handle5?: string
28+
handle2?: string
2129
handle1?: string
2230

2331
// Shadow
@@ -32,10 +40,16 @@ export const Light: Colors = {
3240
background3: Palette.grey300,
3341
background2: Palette.grey200,
3442
background1: Palette.grey100,
43+
background0: Palette.white,
44+
border3: Palette.grey300,
45+
border2: Palette.grey200,
46+
border1: Palette.grey100,
3547
success1: Palette.green400,
3648
success1Hover: Palette.green500,
3749
default1: Palette.grey300,
3850
default1Hover: Palette.grey500,
51+
handle5: Palette.grey500,
52+
handle2: Palette.grey200,
3953
handle1: Palette.white,
4054
shadow1: Palette.black15,
4155
iconBase: Palette.grey700,

src/types/ComponentProps.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import React, { CSSProperties } from 'react'
44
/* Internal dependencies */
55
import { Extendable } from './utilTypes'
66

7-
export type ReactChildren = React.ReactNodeArray | React.ReactNode
8-
97
export interface RenderConfigProps {
108
as?: React.ElementType
119
testId?: string
@@ -22,6 +20,6 @@ export interface ContentComponentProps<Content = React.ReactNode> extends UIComp
2220
content?: Content
2321
}
2422

25-
export interface ChildrenComponentProps<Children = ReactChildren> extends UIComponentProps {
23+
export interface ChildrenComponentProps<Children = React.ReactNode> extends UIComponentProps {
2624
children?: Children
2725
}

0 commit comments

Comments
 (0)