Skip to content

Commit f6db135

Browse files
committed
feat: checkbox
1 parent efa075c commit f6db135

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SVGProps } from 'react'
2+
3+
type CheckIconProps = SVGProps<SVGSVGElement> & {
4+
color: string
5+
}
6+
7+
export function CheckIcon({ color, ...props }: CheckIconProps) {
8+
return (
9+
<svg
10+
height="10"
11+
viewBox="0 0 12 10"
12+
width="12"
13+
xmlns="http://www.w3.org/2000/svg"
14+
{...props}
15+
>
16+
<path
17+
clipRule="evenodd"
18+
d="M11.6474 0.807113C12.0992 1.23373 12.1195 1.94575 11.6929 2.39745L5.31789 9.14745C5.10214 9.37589 4.80069 9.50369 4.48649 9.49992C4.1723 9.49615 3.874 9.36114 3.6638 9.12759L0.288803 5.37759C-0.126837 4.91576 -0.0893993 4.20444 0.372424 3.7888C0.834247 3.37315 1.54557 3.41059 1.96121 3.87242L4.51994 6.71544L10.0571 0.852551C10.4837 0.400843 11.1957 0.3805 11.6474 0.807113Z"
19+
fill={color}
20+
fillRule="evenodd"
21+
/>
22+
</svg>
23+
)
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Meta, StoryObj } from '@storybook/react-vite'
2+
3+
import { Checkbox } from '.'
4+
5+
type Story = StoryObj<typeof meta>
6+
7+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8+
const meta: Meta<typeof Checkbox> = {
9+
title: 'Devfive/Checkbox',
10+
component: Checkbox,
11+
decorators: [
12+
(Story) => (
13+
<div style={{ padding: '10px' }}>
14+
<Story />
15+
</div>
16+
),
17+
],
18+
}
19+
20+
export const Default: Story = {
21+
args: {
22+
children: 'Checkbox',
23+
disabled: false,
24+
},
25+
}
26+
27+
export default meta
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Box, css, Flex, Input, Text } from '@devup-ui/react'
2+
import { ComponentProps } from 'react'
3+
4+
import { CheckIcon } from './CheckIcon'
5+
6+
interface CheckboxProps
7+
extends Omit<ComponentProps<'input'>, 'type' | 'onChange'> {
8+
children: React.ReactNode
9+
onChange?: (checked: boolean) => void
10+
variant?: 'primary' | 'default'
11+
label: string
12+
}
13+
14+
export function Checkbox({
15+
children,
16+
disabled,
17+
checked,
18+
onChange,
19+
variant = 'primary',
20+
label,
21+
...props
22+
}: CheckboxProps) {
23+
return (
24+
<Flex alignItems="center" gap="8px" h="fit-content">
25+
<Box h="18px" pos="relative" w="fit-content">
26+
<Input
27+
_active={
28+
disabled
29+
? undefined
30+
: {
31+
primary: {
32+
bg: 'color-mix(in srgb, var(--primary) 20%, #FFF 80%)',
33+
},
34+
default: {
35+
bg: 'color-mix(in srgb, var(--primary) 30%, #FFF 70%)',
36+
},
37+
}[variant]
38+
}
39+
_checked={{
40+
bg: '$primary',
41+
border: 'none',
42+
_hover: disabled
43+
? undefined
44+
: {
45+
bg:
46+
variant === 'primary'
47+
? 'color-mix(in srgb, var(--primary) 100%, #000 15%)'
48+
: 'color-mix(in srgb, var(--primary) 100%, #FFF 15%)',
49+
},
50+
_disabled: {
51+
bg: '#F0F0F3',
52+
},
53+
}}
54+
_disabled={{
55+
bg: '#F0F0F3',
56+
}}
57+
_hover={
58+
disabled
59+
? undefined
60+
: {
61+
primary: {
62+
bg: 'color-mix(in srgb, var(--primary) 10%, #FFF 90%)',
63+
border: '1px solid var(--primary)',
64+
},
65+
default: {
66+
bg: 'color-mix(in srgb, var(--primary) 100%, #FFF 15%)',
67+
border: '1px solid var(--primary)',
68+
},
69+
}[variant]
70+
}
71+
accentColor="$primary"
72+
appearance="none"
73+
bg="$contentBackground"
74+
border="1px solid var(--border)"
75+
borderRadius="2px"
76+
boxSize="16px"
77+
checked={checked}
78+
cursor={disabled ? 'not-allowed' : 'pointer'}
79+
disabled={disabled}
80+
id={label}
81+
m="0"
82+
onChange={(e) => !disabled && onChange?.(e.target.checked)}
83+
styleOrder={1}
84+
type="checkbox"
85+
{...props}
86+
/>
87+
{checked && (
88+
<CheckIcon
89+
className={css({
90+
position: 'absolute',
91+
top: '8px',
92+
left: '50%',
93+
transform: 'translate(-50%, -50%)',
94+
pointerEvents: 'none',
95+
})}
96+
color={disabled ? '#D6D7DE' : '#FFF'}
97+
/>
98+
)}
99+
</Box>
100+
101+
<label htmlFor={label}>
102+
{typeof children === 'string' ? (
103+
<Text
104+
as="span"
105+
color={disabled ? '#D6D7DE' : '$text'}
106+
cursor={disabled ? 'not-allowed' : 'pointer'}
107+
fontSize="14px"
108+
style={{ userSelect: 'none', verticalAlign: 'middle' }}
109+
>
110+
{children}
111+
</Text>
112+
) : (
113+
children
114+
)}
115+
</label>
116+
</Flex>
117+
)
118+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Meta, StoryObj } from '@storybook/react-vite'
2+
3+
import { CheckboxLayer } from '.'
4+
5+
type Story = StoryObj<typeof meta>
6+
7+
const meta: Meta<typeof CheckboxLayer> = {
8+
title: 'Devfive/CheckboxLayer',
9+
component: CheckboxLayer,
10+
decorators: [
11+
(Story) => (
12+
<div style={{ padding: '20px' }}>
13+
<Story />
14+
</div>
15+
),
16+
],
17+
argTypes: {
18+
onCheckboxChange: { action: 'checkbox changed' },
19+
},
20+
}
21+
22+
export const RowLayout: Story = {
23+
args: {
24+
checkboxes: [
25+
{ id: 'option1', value: '옵션 1 값', label: '옵션 1' },
26+
{
27+
id: 'option2',
28+
value: (
29+
<span style={{ color: 'blue', fontWeight: 'bold' }}>
30+
파란색 텍스트
31+
</span>
32+
),
33+
label: '옵션 2',
34+
},
35+
{
36+
id: 'option3',
37+
value: (
38+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
39+
<span>🎉</span>
40+
<span>이모지와 텍스트</span>
41+
</div>
42+
),
43+
label: '옵션 3',
44+
},
45+
{ id: 'option4', value: 42, label: '옵션 4', disabled: true },
46+
{
47+
id: 'option5',
48+
value: (
49+
<button style={{ padding: '4px 8px', borderRadius: '4px' }}>
50+
버튼 요소
51+
</button>
52+
),
53+
label: '옵션 5',
54+
disabled: true,
55+
checked: true,
56+
},
57+
],
58+
flexDir: 'row',
59+
defaultCheckedIds: ['option2', 'option5'], // 체크됨, disabled and checked
60+
onCheckboxChange: (event) => {
61+
console.info('체크박스 변경됨:', event)
62+
console.info(
63+
`ID: ${event.id}, Value: ${event.value}, Checked: ${event.checked}`,
64+
)
65+
console.info('전체 선택된 값들:', event.checkedValues)
66+
},
67+
},
68+
}
69+
70+
export const ColumnLayout: Story = {
71+
args: {
72+
checkboxes: [
73+
{ id: 'option1', value: '옵션 1 값', label: '옵션 1' },
74+
{
75+
id: 'option2',
76+
value: (
77+
<span style={{ color: 'blue', fontWeight: 'bold' }}>
78+
파란색 텍스트
79+
</span>
80+
),
81+
label: '옵션 2',
82+
},
83+
{
84+
id: 'option3',
85+
value: (
86+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
87+
<span>🎉</span>
88+
<span>이모지와 텍스트</span>
89+
</div>
90+
),
91+
label: '옵션 3',
92+
},
93+
{ id: 'option4', value: 42, label: '옵션 4', disabled: true },
94+
{
95+
id: 'option5',
96+
value: (
97+
<button style={{ padding: '4px 8px', borderRadius: '4px' }}>
98+
버튼 요소
99+
</button>
100+
),
101+
label: '옵션 5',
102+
disabled: true,
103+
checked: true,
104+
},
105+
],
106+
flexDir: 'column',
107+
defaultCheckedIds: ['option2', 'option5'], // 체크됨, disabled and checked
108+
onCheckboxChange: (event) => {
109+
console.info('체크박스 변경됨:', event)
110+
console.info(
111+
`ID: ${event.id}, Value: ${event.value}, Checked: ${event.checked}`,
112+
)
113+
console.info('전체 선택된 값들:', event.checkedValues)
114+
},
115+
},
116+
}
117+
118+
export default meta
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Flex } from '@devup-ui/react'
2+
import { useState } from 'react'
3+
4+
import { Checkbox } from '../Checkbox'
5+
6+
export interface CheckboxItem {
7+
id: string
8+
value: React.ReactNode
9+
label: string
10+
disabled?: boolean
11+
checked?: boolean
12+
}
13+
14+
export interface CheckboxChangeEvent {
15+
id: string
16+
value: React.ReactNode
17+
checked: boolean
18+
checkedValues: React.ReactNode[]
19+
}
20+
21+
export interface CheckBoxLayerProps {
22+
checkboxes: CheckboxItem[]
23+
flexDir: 'row' | 'column'
24+
gap?: number
25+
onCheckboxChange?: (event: CheckboxChangeEvent) => void
26+
defaultCheckedIds?: string[]
27+
variant?: 'primary' | 'default'
28+
}
29+
30+
export function CheckboxLayer({
31+
checkboxes,
32+
flexDir,
33+
gap,
34+
onCheckboxChange,
35+
defaultCheckedIds = [],
36+
variant = 'primary',
37+
}: CheckBoxLayerProps) {
38+
const [checkedIds, setCheckedIds] = useState<string[]>(defaultCheckedIds)
39+
40+
const handleCheckboxChange = (
41+
id: string,
42+
value: React.ReactNode,
43+
checked: boolean,
44+
) => {
45+
const updatedIds = checked
46+
? [...checkedIds, id]
47+
: checkedIds.filter((checkedId) => checkedId !== id)
48+
49+
setCheckedIds(updatedIds)
50+
51+
const checkedValues = updatedIds
52+
.map((checkedId) => checkboxes.find((cb) => cb.id === checkedId)?.value)
53+
.filter((val): val is React.ReactNode => val !== undefined)
54+
55+
onCheckboxChange?.({
56+
id,
57+
value,
58+
checked,
59+
checkedValues,
60+
})
61+
}
62+
63+
return (
64+
<Flex flexDir={flexDir} gap={gap || (flexDir === 'row' ? '30px' : '16px')}>
65+
{checkboxes.map((checkbox) => (
66+
<Checkbox
67+
key={checkbox.id}
68+
checked={checkedIds.includes(checkbox.id)}
69+
disabled={checkbox.disabled}
70+
label={`${checkbox.id}-${checkbox.label}`} // 고유한 label 생성
71+
onChange={(checked) =>
72+
handleCheckboxChange(checkbox.id, checkbox.value, checked)
73+
}
74+
variant={variant}
75+
>
76+
{checkbox.value}
77+
</Checkbox>
78+
))}
79+
</Flex>
80+
)
81+
}

0 commit comments

Comments
 (0)