Skip to content

Commit f712fbb

Browse files
authored
[#41] ✨ 공통 컴포넌트 Chip 개발 (#82)
* [#41] ✨ add label types to team.d.ts and community.d.ts * [#41] 🚚 move chip components in components directory * [#41] 🚚 move chip in common/chip * [#41] 💄 correct few styles along to its label * [#41] ✅ add Chip story * [#41] ✅ add chip test codes * [#41] 🐛 fix twMerge class name merging issues along to the test result * [#20] 💄 add lineheight and letter spacing to fontPallete * [#41] 🗑️ remove unused package * [#41] 💄 roll back default bg text styles to base style * [#41] ✨ add deletable chip component * [#41] ✨ add module bridge index.ts * [#41] 💄 turn ic close svg into currentColor * [#41] ✅ add deletable story * [#41] ✅ add deletable test + mock jest with svg
1 parent f0d6d6e commit f712fbb

File tree

14 files changed

+308
-28
lines changed

14 files changed

+308
-28
lines changed

jest.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const config: Config = {
2121
],
2222
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
2323
moduleNameMapper: {
24-
'^@/(.*)$': '<rootDir>/src/$1',
24+
'\\.svg$': '<rootDir>/src/__mocks__/fileMock.js', // SVG 파일을 모킹
25+
'^@/(.*)$': '<rootDir>/src/$1', // 다른 경로 매핑
2526
},
2627
}
2728

src/__mocks__/fileMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'test-file-stub'

src/assets/icons/ic-close.svg

Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import '@testing-library/jest-dom'
2+
import { render, screen } from '@testing-library/react'
3+
4+
import { Chip } from './Chip'
5+
6+
describe('Chip Component', () => {
7+
test('renders the Chip component with correct label', () => {
8+
// Chip 컴포넌트가 올바른 라벨을 렌더링하는지 확인합니다.
9+
render(<Chip label='모집 중' />)
10+
const chipElement = screen.getByText(/ /i)
11+
expect(chipElement).toBeInTheDocument()
12+
})
13+
14+
test('applies correct styles for each label', () => {
15+
// 각 라벨에 대해 올바른 스타일이 적용되는지 확인합니다.
16+
const labelStyles = {
17+
'모집 중': 'bg-blue-100 text-blue-500',
18+
'모집 완료': 'bg-gray-200 text-gray-600',
19+
스터디: 'bg-green-100 text-green-500',
20+
프로젝트: 'bg-purple-100 text-purple-500',
21+
멘토링: 'bg-red-100 text-red-500',
22+
기술: 'bg-blue-100 text-blue-500',
23+
커리어: 'bg-pink-100 text-pink-500',
24+
기타: 'bg-orange-100 text-orange-500',
25+
}
26+
27+
Object.entries(labelStyles).forEach(([label, expectedClasses]) => {
28+
render(<Chip label={label} />)
29+
const chipElement = screen.getByText(label)
30+
31+
// expectedClasses에 포함된 각 클래스가 chipElement에 포함되는지 확인합니다.
32+
expectedClasses.split(' ').forEach(className => {
33+
expect(chipElement).toHaveClass(className)
34+
})
35+
})
36+
})
37+
38+
test('applies additional className passed as prop', () => {
39+
// className prop을 통해 추가된 클래스가 Chip 컴포넌트에 적용되는지 확인합니다.
40+
render(<Chip label='기술' className='custom-class' />)
41+
const chipElement = screen.getByText(//i)
42+
expect(chipElement).toHaveClass('custom-class')
43+
})
44+
45+
test('applies base styles correctly', () => {
46+
// 기본 스타일이 Chip 컴포넌트에 올바르게 적용되는지 확인합니다.
47+
render(<Chip label='커리어' />)
48+
const chipElement = screen.getByText(//i)
49+
expect(chipElement).toHaveClass(
50+
'flex',
51+
'h-28',
52+
'items-center',
53+
'justify-center',
54+
'rounded-4',
55+
'px-6',
56+
'text-body3',
57+
'font-medium'
58+
)
59+
})
60+
61+
test('applies default styles when label is not in styleByLabel', () => {
62+
// styleByLabel에 없는 라벨에 대해 기본 스타일이 적용되는지 확인합니다.
63+
render(<Chip label='기타 라벨' />)
64+
const chipElement = screen.getByText(/ /i)
65+
expect(chipElement).toHaveClass('bg-gray-200', 'text-gray-500')
66+
})
67+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import clsx from 'clsx'
2+
3+
type ChipProps = {
4+
label: string
5+
className?: string
6+
}
7+
8+
const baseStyle =
9+
'flex h-28 items-center justify-center rounded-4 bg-gray-200 px-6 text-body3 font-medium text-gray-500'
10+
11+
const styleByLabel: Record<string, string> = {
12+
'모집 중': 'bg-blue-100 text-blue-500',
13+
'모집 완료': 'bg-gray-200 text-gray-600',
14+
스터디: 'bg-green-100 text-green-500',
15+
프로젝트: 'bg-purple-100 text-purple-500',
16+
멘토링: 'bg-red-100 text-red-500',
17+
기술: 'bg-blue-100 text-blue-500',
18+
커리어: 'bg-pink-100 text-pink-500',
19+
기타: 'bg-orange-100 text-orange-500',
20+
}
21+
22+
export const Chip = ({ label, className = '' }: ChipProps): JSX.Element => {
23+
const labelStyle = styleByLabel[label] || ''
24+
const chipStyle = clsx(baseStyle, labelStyle, className)
25+
return <span className={chipStyle}>{label}</span>
26+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import '@testing-library/jest-dom'
2+
import { fireEvent, render, screen } from '@testing-library/react'
3+
4+
import { DeletableChip } from './DeletableChip'
5+
6+
describe('DeletableChip Component', () => {
7+
test('renders the chip with correct label', () => {
8+
// 올바른 label이 렌더링되는지 확인합니다.
9+
render(<DeletableChip label='스터디' onDelete={() => {}} />)
10+
const chipElement = screen.getByText(//i)
11+
expect(chipElement).toBeInTheDocument()
12+
})
13+
14+
test('renders with the default blue color style', () => {
15+
// 기본 색상이 blue로 렌더링되는지 확인합니다.
16+
render(<DeletableChip label='스터디' onDelete={() => {}} />)
17+
const chipElement = screen.getByText(//i)
18+
expect(chipElement).toHaveClass('bg-blue-800', 'text-common-white')
19+
})
20+
21+
test('renders with the gray color style when specified', () => {
22+
// gray 색상이 지정되었을 때 올바르게 스타일링되는지 확인합니다.
23+
render(<DeletableChip label='스터디' color='gray' onDelete={() => {}} />)
24+
const chipElement = screen.getByText(//i)
25+
expect(chipElement).toHaveClass(
26+
'border-1',
27+
'border-solid',
28+
'border-gray-200',
29+
'bg-gray-100',
30+
'text-gray-700'
31+
)
32+
})
33+
34+
test('renders delete button with gray color style when chip color is gray', () => {
35+
// chip이 gray일 때 delete 버튼이 gray 스타일을 가지는지 확인합니다.
36+
render(<DeletableChip label='스터디' color='gray' onDelete={() => {}} />)
37+
const buttonElement = screen.getByRole('button', { name: / /i })
38+
expect(buttonElement).toHaveClass('text-gray-400')
39+
})
40+
41+
test('calls onDelete when delete button is clicked', () => {
42+
// 삭제 버튼 클릭 시 onDelete가 호출되는지 확인합니다.
43+
const handleDelete = jest.fn()
44+
render(<DeletableChip label='스터디' onDelete={handleDelete} />)
45+
const buttonElement = screen.getByRole('button', { name: / /i })
46+
fireEvent.click(buttonElement)
47+
expect(handleDelete).toHaveBeenCalledTimes(1)
48+
})
49+
50+
test('applies additional className when specified', () => {
51+
// 추가된 className이 적용되는지 확인합니다.
52+
render(
53+
<DeletableChip
54+
label='스터디'
55+
onDelete={() => {}}
56+
className='custom-class'
57+
/>
58+
)
59+
const chipElement = screen.getByText(//i)
60+
expect(chipElement).toHaveClass('custom-class')
61+
})
62+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { IcClose } from '@/assets/IconList'
2+
import clsx from 'clsx'
3+
4+
type Color = 'blue' | 'gray'
5+
6+
type ChipProps = {
7+
onDelete: () => void
8+
color?: Color
9+
label: string
10+
className?: string
11+
}
12+
13+
const baseStyle =
14+
'flex h-36 items-center justify-center gap-4 rounded-4 px-8 text-body2 font-medium'
15+
16+
const styleByColor = {
17+
blue: 'bg-blue-800 text-common-white',
18+
gray: 'border-1 border-solid border-gray-200 bg-gray-100 text-gray-700',
19+
}
20+
21+
export const DeletableChip = ({
22+
onDelete,
23+
label,
24+
color = 'blue',
25+
className = '',
26+
}: ChipProps): JSX.Element => {
27+
const chipStyle = clsx(baseStyle, styleByColor[color], className)
28+
const buttonStyle = clsx({ 'text-gray-400': color === 'gray' })
29+
return (
30+
<span className={chipStyle}>
31+
{label}
32+
<button
33+
onClick={onDelete}
34+
className={buttonStyle}
35+
type='button'
36+
aria-label={`${label} 삭제`}
37+
>
38+
<IcClose />
39+
</button>
40+
</span>
41+
)
42+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { Chip } from './Chip'
2+
import { DeletableChip } from './DeletableChip'
3+
4+
export { Chip, DeletableChip }
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { fn } from '@storybook/test'
2+
3+
import { Chip } from '@/components/common/chip'
4+
5+
export default {
6+
title: 'Common/Chip/Chip',
7+
component: Chip,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['common', 'chip'],
12+
argTypes: {
13+
label: {
14+
control: 'radio',
15+
options: [
16+
'모집 중',
17+
'모집 완료',
18+
'스터디',
19+
'프로젝트',
20+
'멘토링',
21+
'기술',
22+
'커리어',
23+
'기타',
24+
'프론트엔드',
25+
'#Spring',
26+
],
27+
description: '문자열만 받습니다.',
28+
},
29+
},
30+
args: {
31+
onClick: fn(),
32+
},
33+
}
34+
35+
export const Default = {
36+
args: {
37+
label: '스터디',
38+
},
39+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { fn } from '@storybook/test'
2+
3+
import { DeletableChip } from '@/components/common/chip'
4+
5+
export default {
6+
title: 'Common/Chip/DeletableChip',
7+
component: DeletableChip,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['common', 'chip', 'deletable'],
12+
argTypes: {
13+
label: {
14+
control: 'radio',
15+
options: [
16+
'Spring',
17+
'ReactNative',
18+
'MongoDB',
19+
'TypeScript',
20+
'프론트엔드',
21+
'풀스택',
22+
'DevOps 엔지니어',
23+
],
24+
description: '문자열만 받습니다.',
25+
},
26+
},
27+
args: {
28+
onDelete: fn(),
29+
},
30+
}
31+
32+
export const Default = {
33+
args: {
34+
label: '스터디',
35+
},
36+
}
37+
38+
export const Gray = {
39+
args: {
40+
label: '기술',
41+
color: 'gray',
42+
},
43+
}

0 commit comments

Comments
 (0)