Skip to content

Commit 0040f18

Browse files
authored
[#45] ✨ 공통 컴포넌트 CheckboxInput 구현 (#105)
* [#45] ✨ Implementing CheckboxInput component (WIP) * [#45] ✨ Add required images files for CheckboxInput component * [#45] 🔧 Add new images to iconList * [#45] ✨ Add toggleCheckbox utility func * [#45] ✨ Implement CheckboxInput component * [#45] ✅ Add Storybook test code for CheckboxInput component * [#45] 🐛 Fix build error by replacing 'any' with precise type definition * [#45] ✨ Add index file to export all Input components * [#45] ✨ Add e.preventDefault() and explicit Enter key handling * [#44] ♻️ Change span to button for better accessbility in clickable elements * [#45] ♻️ Refactor functions and improve accessibility
1 parent 945e784 commit 0040f18

File tree

11 files changed

+209
-3
lines changed

11 files changed

+209
-3
lines changed

src/assets/IconList.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import IcBin from './icons/ic-bin.svg'
44
import IcCalendar from './icons/ic-calendar.svg'
55
import IcCaretDown from './icons/ic-caret-down.svg'
66
import IcCaretUp from './icons/ic-caret-up.svg'
7+
import IcCheckOff from './icons/ic-check-off.svg'
8+
import IcCheckOn from './icons/ic-check-on.svg'
79
import IcCheck from './icons/ic-check.svg'
10+
import IcCheckboxOff from './icons/ic-checkbox-off.svg'
11+
import IcCheckboxOn from './icons/ic-checkbox-on.svg'
812
import IcChevronLeft from './icons/ic-chevron-left.svg'
913
import IcChevronRight from './icons/ic-chevron-right.svg'
1014
import IcClose from './icons/ic-close.svg'
@@ -70,4 +74,8 @@ export {
7074
IcSearch,
7175
IcShare,
7276
IcStart,
77+
IcCheckboxOn,
78+
IcCheckboxOff,
79+
IcCheckOn,
80+
IcCheckOff,
7381
}

src/assets/icons/ic-check-off.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/ic-check-on.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
IcCheckOff,
3+
IcCheckOn,
4+
IcCheckboxOff,
5+
IcCheckboxOn,
6+
} from '@/assets/IconList'
7+
import clsx from 'clsx'
8+
import { twMerge } from 'tailwind-merge'
9+
10+
import { handleKeyDown } from '@/utils/handleKeyDown'
11+
import { toggleCheckbox } from '@/utils/toggleCheckbox'
12+
13+
export interface CheckboxInputProps
14+
extends React.InputHTMLAttributes<HTMLInputElement> {
15+
label: string
16+
variant: 'checkbox' | 'check'
17+
}
18+
19+
export const CheckboxInput = ({
20+
label,
21+
variant = 'checkbox',
22+
className = '',
23+
checked = false,
24+
disabled,
25+
onChange,
26+
...props
27+
}: CheckboxInputProps): JSX.Element => {
28+
const getCheckboxIcon = (checked: boolean) => {
29+
return checked ? (
30+
<IcCheckboxOn width={24} height={24} alt='체크된 체크박스' />
31+
) : (
32+
<IcCheckboxOff width={24} height={24} alt='체크 안 된 체크박스' />
33+
)
34+
}
35+
36+
const getCheckIcon = (checked: boolean) => {
37+
return checked ? (
38+
<IcCheckOn width={24} height={24} alt='체크된 체크' />
39+
) : (
40+
<IcCheckOff width={24} height={24} alt='체크 안 된 체크' />
41+
)
42+
}
43+
44+
const getIconForState = (variant: string, checked: boolean) => {
45+
if (variant === 'checkbox') {
46+
return getCheckboxIcon(checked)
47+
}
48+
return getCheckIcon(checked)
49+
}
50+
51+
const handleToggle = () => {
52+
if (!disabled) {
53+
toggleCheckbox(checked, onChange, props.value)
54+
}
55+
}
56+
57+
const labelClass = clsx('flex items-center', disabled && 'opacity-50')
58+
const buttonClass = twMerge(
59+
'focus:outline-none focus:ring-1 focus:ring-primary-normal',
60+
disabled && 'cursor-not-allowed opacity-50'
61+
)
62+
const labelTextClass = twMerge('ml-10 h-22', className)
63+
64+
return (
65+
<label className={labelClass}>
66+
<input
67+
type='checkbox'
68+
checked={checked}
69+
disabled={disabled}
70+
{...props}
71+
className='hidden'
72+
/>
73+
<button
74+
role='checkbox'
75+
tabIndex={0}
76+
aria-checked={checked}
77+
aria-label={'checkbox button'}
78+
onKeyDown={e => handleKeyDown(e, handleToggle, disabled)}
79+
onClick={handleToggle}
80+
className={buttonClass}
81+
>
82+
{getIconForState(variant, checked)}
83+
</button>
84+
<span className={labelTextClass}>{label}</span>
85+
</label>
86+
)
87+
}

src/components/common/input/RadioInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const RadioInput = ({
2323
return (
2424
<label className={labelClass}>
2525
<input type='radio' checked={checked} disabled={disabled} {...props} />
26-
<span
26+
<button
2727
role='radio'
2828
tabIndex={0}
2929
aria-checked={checked}
@@ -34,7 +34,7 @@ export const RadioInput = ({
3434
() =>
3535
onChange?.({
3636
target: { checked: true, value: props.value },
37-
} as any),
37+
} as React.ChangeEvent<HTMLInputElement>),
3838
disabled
3939
)
4040
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { CheckboxInput } from './CheckboxInput'
2+
import { PasswordInput } from './PasswordInput'
3+
import { RadioInput } from './RadioInput'
4+
import { TagInput } from './TagInput'
5+
import { TextInput } from './TextInput'
6+
7+
export { CheckboxInput, PasswordInput, RadioInput, TagInput, TextInput }
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Meta, StoryObj } from '@storybook/react'
2+
import { useState } from 'react'
3+
4+
import { CheckboxInput } from '@/components/common/input/CheckboxInput'
5+
import type { CheckboxInputProps } from '@/components/common/input/CheckboxInput'
6+
7+
const meta: Meta<typeof CheckboxInput> = {
8+
component: CheckboxInput,
9+
title: 'Common/Input/CheckboxInput',
10+
args: {
11+
label: '전체 동의',
12+
variant: 'checkbox',
13+
disabled: false,
14+
},
15+
}
16+
17+
export default meta
18+
19+
type Story = StoryObj<typeof CheckboxInput>
20+
21+
export const Default: Story = {
22+
args: {
23+
label: '전체 동의',
24+
variant: 'checkbox',
25+
checked: false,
26+
disabled: false,
27+
},
28+
render: function Render(args: CheckboxInputProps) {
29+
const [checked, setChecked] = useState(false)
30+
31+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32+
setChecked(e.target.checked)
33+
}
34+
35+
return <CheckboxInput {...args} checked={checked} onChange={handleChange} />
36+
},
37+
}
38+
39+
export const Check: Story = {
40+
args: {
41+
label: '이메일 수신 동의',
42+
variant: 'check',
43+
checked: false,
44+
disabled: false,
45+
},
46+
render: function Render(args: CheckboxInputProps) {
47+
const [checked, setChecked] = useState(false)
48+
49+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
50+
setChecked(e.target.checked)
51+
}
52+
53+
return <CheckboxInput {...args} checked={checked} onChange={handleChange} />
54+
},
55+
}
56+
57+
export const DisabledCheckbox: Story = {
58+
args: {
59+
label: '비활성화된 체크박스',
60+
variant: 'checkbox',
61+
checked: false,
62+
disabled: true,
63+
},
64+
render: function Render(args: CheckboxInputProps) {
65+
return <CheckboxInput {...args} />
66+
},
67+
}
68+
69+
export const DisabledCheck: Story = {
70+
args: {
71+
label: '비활성화된 체크',
72+
variant: 'check',
73+
checked: false,
74+
disabled: true,
75+
},
76+
render: function Render(args: CheckboxInputProps) {
77+
return <CheckboxInput {...args} />
78+
},
79+
}

src/utils/handleKeyDown.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ export const handleKeyDown = (
33
callback: () => void,
44
disabled?: boolean
55
): void => {
6-
if (e.key === 'Enter' && !disabled) {
6+
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
7+
e.preventDefault()
78
callback()
89
}
910
}

0 commit comments

Comments
 (0)