Skip to content

Commit 394c5e9

Browse files
authored
[#102] ✨ shared 컴포넌트 Select 개발 (#122)
* [#50] 🚚 rename index.tsx -> index.ts * [#67] 💄 solve prettier build warnings by formatting * [#97] ✨ add dropdown component * [#97] ♻️ add close method to useToggle hook * [#97] ✅ add dropdown stories * [#97] 🔧 add icon assets (setting / workbag / codefile / logout) * [#97] 💄 adjust svg fill and stroke attributes * [#97] 💄 replace size props from 44 px to 48px in avatar * [#97] 💄 expand highlight component with new className props * [#97] ✨ add header user menu component * [#97] ✅ add header user menu stories * [#97] 🐛 remove unused value props from dropdown items * [#97] 🚚 remove comments and allow close method when touching outside of dropdown * [#97] 🚚 remove closeOnSelect props from header user menu component * [#97] 🗑️ remove select component * [#97] 🐛 solve build warning by removing unused import line * [#97] ✨ make sure dropdown to close when clicked outside of the menu * [#102] 💄 update caret up svg style * [#102] ♻️ allow using useDropdownContext as a module * [#102] ✨ add select component * [#102] ✅ add select stories * [#102] ♻️ simplify classname by declaring vars * [#102] 💄 control width in dropdown trigger box instead of trigger * [#102] 🚚 rename dropdown trigger box props * [#102] ♻️ remove duplicated state control * [#102] 🐛 remove unused arguments in handleSelect * [#102] ♻️ isOpon state controles aria-expanded of dropdown trigger * [#102] 🚚 renmae story paths in select stories * [#102] ✨ add multiple select component * [#102] ✅ add multi select stories * [#102] 🐛 fix build error by erasing dependency list in dropdown * [#102] 🗑️ remove React import
1 parent ada42ae commit 394c5e9

File tree

10 files changed

+329
-12
lines changed

10 files changed

+329
-12
lines changed

src/assets/icons/ic-caret-up.svg

Lines changed: 1 addition & 1 deletion
Loading

src/components/common/button/Clickable.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from 'react'
2-
31
import '@testing-library/jest-dom'
42
import { render, screen } from '@testing-library/react'
53

src/components/common/dropdown/Dropdown.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from 'react'
2-
31
import '@testing-library/jest-dom'
42
import { fireEvent, render, screen } from '@testing-library/react'
53

src/components/common/dropdown/Dropdown.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ interface DropdownContextType {
1515

1616
const DropdownContext = createContext<DropdownContextType | null>(null)
1717

18-
const useDropdownContext = () => {
18+
export const useDropdownContext = (): DropdownContextType => {
1919
const context = useContext(DropdownContext)
20+
2021
if (!context) {
2122
throw new Error('useDropdownContext must be used within a DropdownProvider')
2223
}
@@ -46,7 +47,7 @@ const Dropdown = ({ children, className }: BaseProps): JSX.Element => {
4647
return () => {
4748
document.removeEventListener('mousedown', handleClickOutside)
4849
}
49-
}, [])
50+
})
5051

5152
return (
5253
<DropdownContext.Provider value={{ isOpen, toggle, close }}>
@@ -58,15 +59,15 @@ const Dropdown = ({ children, className }: BaseProps): JSX.Element => {
5859
}
5960

6061
const Trigger = ({ children, className }: BaseProps) => {
61-
const { toggle } = useDropdownContext()
62+
const { isOpen, toggle } = useDropdownContext()
6263

6364
return (
6465
<button
6566
type='button'
6667
className={className}
6768
onClick={toggle}
6869
aria-haspopup='listbox'
69-
aria-expanded={true}
70+
aria-expanded={isOpen}
7071
>
7172
{children}
7273
</button>
@@ -112,7 +113,7 @@ interface ItemProps extends BaseProps {
112113

113114
const getItemStyle = (className: string) =>
114115
clsx(
115-
'flex h-40 w-full items-center rounded-8 px-12 text-body2 text-gray-800 hover:bg-gray-100',
116+
'flex h-40 w-full items-center rounded-8 px-12 text-body2 font-medium text-gray-800 hover:bg-gray-100',
116117
className
117118
)
118119

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { Dropdown } from './Dropdown'
1+
import { Dropdown, useDropdownContext } from './Dropdown'
22

3-
export { Dropdown }
3+
export { Dropdown, useDropdownContext }
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { IcCaretDown, IcCaretUp } from '@/assets/IconList'
2+
import clsx from 'clsx'
3+
4+
import { Box } from '@/components/common/containers'
5+
import { Dropdown, useDropdownContext } from '@/components/common/dropdown'
6+
import { CheckboxInput } from '@/components/common/input'
7+
8+
type Option = {
9+
label: string
10+
value: string
11+
}
12+
13+
interface SelectProps {
14+
options: Option[]
15+
values: string[]
16+
onChange: React.Dispatch<React.SetStateAction<string[]>>
17+
placeholder?: string
18+
disabled?: boolean
19+
}
20+
export const MultiSelect = ({
21+
options,
22+
values,
23+
onChange,
24+
placeholder = 'Select an option',
25+
disabled = false,
26+
}: SelectProps): JSX.Element => {
27+
const selectedOptions = options.filter(option =>
28+
values.includes(option.value)
29+
)
30+
const selectedLabel = selectedOptions[0]?.value
31+
? `${selectedOptions[0]?.value}` +
32+
(selectedOptions.length - 1
33+
? ` 외 ${selectedOptions.length - 1}개 선택`
34+
: '')
35+
: ''
36+
37+
const handleSelect = (value: string) => {
38+
onChange(prev =>
39+
prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]
40+
)
41+
}
42+
43+
const triggerStyle = clsx({
44+
'pointer-events-none cursor-not-allowed': disabled,
45+
})
46+
47+
return (
48+
<Dropdown>
49+
<Dropdown.Trigger className={triggerStyle} aria-disabled={disabled}>
50+
<DropdownTriggerBox
51+
label={selectedLabel || placeholder}
52+
isSelected={!!selectedLabel}
53+
isDisabled={disabled}
54+
/>
55+
</Dropdown.Trigger>
56+
<Dropdown.Menu>
57+
{options.map(option => (
58+
<Dropdown.Item
59+
key={option.value}
60+
closeOnSelect={false}
61+
onClick={() => handleSelect(option.value)}
62+
>
63+
<CheckboxInput
64+
label={option.label}
65+
onClick={() => handleSelect(option.value)}
66+
variant='checkbox'
67+
checked={selectedOptions.some(
68+
selectedOption => selectedOption.value === option.value
69+
)}
70+
className='ml-4 cursor-pointer'
71+
/>
72+
</Dropdown.Item>
73+
))}
74+
</Dropdown.Menu>
75+
</Dropdown>
76+
)
77+
}
78+
79+
interface DropdownTriggerBoxProps {
80+
label: string
81+
isSelected: boolean
82+
isDisabled: boolean
83+
}
84+
85+
const DropdownTriggerBox = ({
86+
label,
87+
isSelected,
88+
isDisabled,
89+
}: DropdownTriggerBoxProps) => {
90+
const { isOpen } = useDropdownContext()
91+
const triggerBoxClass = clsx(
92+
'h-48 w-210 justify-between p-12 text-body1 font-medium text-gray-500 focus:border-primary-normal',
93+
{ 'text-gray-800': isSelected },
94+
{ 'bg-gray-200 text-gray-400': isDisabled }
95+
)
96+
97+
return (
98+
<Box className={triggerBoxClass} rounded={8}>
99+
{label}
100+
{isOpen ? <IcCaretUp /> : <IcCaretDown />}
101+
</Box>
102+
)
103+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { IcCaretDown, IcCaretUp } from '@/assets/IconList'
2+
import clsx from 'clsx'
3+
4+
import { Box } from '@/components/common/containers'
5+
import { Dropdown, useDropdownContext } from '@/components/common/dropdown'
6+
7+
type Options = {
8+
label: string
9+
value: string
10+
}
11+
12+
interface SelectProps {
13+
options: Options[]
14+
value: string
15+
onChange: (value: string) => void
16+
placeholder?: string
17+
disabled?: boolean
18+
}
19+
export const Select = ({
20+
options,
21+
value,
22+
onChange,
23+
placeholder = 'Select an option',
24+
disabled = false,
25+
}: SelectProps): JSX.Element => {
26+
const selectedOption = options.find(option => option.value === value)
27+
const selectedLabel = selectedOption?.label || ''
28+
29+
const handleSelect = (value: string) => {
30+
onChange(value)
31+
}
32+
33+
const triggerStyle = clsx({
34+
'pointer-events-none cursor-not-allowed': disabled,
35+
})
36+
37+
return (
38+
<Dropdown>
39+
<Dropdown.Trigger className={triggerStyle} aria-disabled={disabled}>
40+
<DropdownTriggerBox
41+
label={selectedLabel || placeholder}
42+
isSelected={!!selectedLabel}
43+
isDisabled={disabled}
44+
/>
45+
</Dropdown.Trigger>
46+
<Dropdown.Menu>
47+
{options.map(option => (
48+
<Dropdown.Item
49+
key={option.value}
50+
onClick={() => handleSelect(option.value)}
51+
>
52+
{option.label}
53+
</Dropdown.Item>
54+
))}
55+
</Dropdown.Menu>
56+
</Dropdown>
57+
)
58+
}
59+
60+
interface DropdownTriggerBoxProps {
61+
label: string
62+
isSelected: boolean
63+
isDisabled: boolean
64+
}
65+
66+
const DropdownTriggerBox = ({
67+
label,
68+
isSelected,
69+
isDisabled,
70+
}: DropdownTriggerBoxProps) => {
71+
const { isOpen } = useDropdownContext()
72+
const triggerBoxClass = clsx(
73+
'h-48 w-210 justify-between p-12 text-body1 font-medium text-gray-500 focus:border-primary-normal',
74+
{ 'text-gray-800': isSelected },
75+
{ 'bg-gray-200 text-gray-400': isDisabled }
76+
)
77+
78+
return (
79+
<Box className={triggerBoxClass} rounded={8}>
80+
{label}
81+
{isOpen ? <IcCaretUp /> : <IcCaretDown />}
82+
</Box>
83+
)
84+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { MultiSelect } from './MultiSelect'
2+
import { Select } from './Select'
3+
4+
export { Select, MultiSelect }
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useState } from 'react'
2+
3+
import { Meta, StoryFn } from '@storybook/react'
4+
5+
import { MultiSelect } from '@/components/shared/select'
6+
7+
export default {
8+
title: 'Shared/Select/MultiSelect',
9+
component: MultiSelect,
10+
parameters: {
11+
layout: 'centered',
12+
},
13+
} as Meta
14+
15+
const Template: StoryFn<typeof MultiSelect> = args => {
16+
const [values, setValues] = useState<string[]>(args.values || [])
17+
return <MultiSelect {...args} values={values} onChange={setValues} />
18+
}
19+
20+
export const Default = Template.bind({})
21+
Default.args = {
22+
options: [
23+
{ label: '백엔드', value: '백엔드' },
24+
{ label: '프론트엔드', value: '프론트엔드' },
25+
{ label: '모바일', value: '모바일' },
26+
{ label: '기타', value: '기타' },
27+
],
28+
values: [],
29+
placeholder: '포지션',
30+
disabled: false,
31+
}
32+
33+
export const WithPreSelectedValue = Template.bind({})
34+
WithPreSelectedValue.args = {
35+
options: [
36+
{ label: 'Option 1', value: 'option1' },
37+
{ label: 'Option 2', value: 'option2' },
38+
{ label: 'Option 3', value: 'option3' },
39+
],
40+
values: ['option2'],
41+
placeholder: 'Select an option',
42+
disabled: false,
43+
}
44+
45+
export const Disabled = Template.bind({})
46+
Disabled.args = {
47+
options: [
48+
{ label: 'Option 1', value: 'option1' },
49+
{ label: 'Option 2', value: 'option2' },
50+
{ label: 'Option 3', value: 'option3' },
51+
],
52+
values: [],
53+
placeholder: 'Select an option',
54+
disabled: true,
55+
}
56+
57+
export const MultiSelected = Template.bind({})
58+
MultiSelected.args = {
59+
options: [
60+
{ label: '스터디', value: '스터디' },
61+
{ label: '팀 프로젝트', value: '팀 프로젝트' },
62+
{ label: '멘토링', value: '멘토링' },
63+
],
64+
values: ['스터디', '팀 프로젝트'],
65+
placeholder: '모집 유형 선택',
66+
disabled: false,
67+
}
68+
69+
export const NoOptionsAvailable = Template.bind({})
70+
NoOptionsAvailable.args = {
71+
options: [],
72+
values: [],
73+
placeholder: '선택할 옵션이 없습니다',
74+
disabled: false,
75+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState } from 'react'
2+
3+
import { Meta, StoryFn } from '@storybook/react'
4+
5+
import { Select } from '@/components/shared/select'
6+
7+
export default {
8+
title: 'Shared/Select/Select',
9+
component: Select,
10+
parameters: {
11+
layout: 'centered',
12+
},
13+
} as Meta
14+
15+
const Template: StoryFn<typeof Select> = args => {
16+
const [value, setValue] = useState(args.value)
17+
return <Select {...args} value={value} onChange={setValue} />
18+
}
19+
20+
export const Default = Template.bind({})
21+
Default.args = {
22+
options: [
23+
{ label: '스터디', value: '스터디' },
24+
{ label: '팀 프로젝트', value: '팀 프로젝트' },
25+
{ label: '멘토링', value: '멘토링' },
26+
],
27+
value: '',
28+
placeholder: '모집 유형 선택',
29+
disabled: false,
30+
}
31+
32+
export const WithPreSelectedValue = Template.bind({})
33+
WithPreSelectedValue.args = {
34+
options: [
35+
{ label: 'Option 1', value: 'option1' },
36+
{ label: 'Option 2', value: 'option2' },
37+
{ label: 'Option 3', value: 'option3' },
38+
],
39+
value: 'option2',
40+
placeholder: 'Select an option',
41+
disabled: false,
42+
}
43+
44+
export const Disabled = Template.bind({})
45+
Disabled.args = {
46+
options: [
47+
{ label: 'Option 1', value: 'option1' },
48+
{ label: 'Option 2', value: 'option2' },
49+
{ label: 'Option 3', value: 'option3' },
50+
],
51+
value: '',
52+
placeholder: 'Select an option',
53+
disabled: true,
54+
}

0 commit comments

Comments
 (0)