Skip to content

Commit c992a7a

Browse files
committed
Fix select componenet
1 parent 44b5aea commit c992a7a

File tree

9 files changed

+146
-48
lines changed

9 files changed

+146
-48
lines changed

packages/components/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"lint": "eslint",
2323
"build": "tsc && vite build",
2424
"storybook": "storybook dev -p 6006",
25-
"build-storybook": "storybook build"
25+
"build-storybook": "storybook build",
26+
"package": "pnpm pack --pack-destination \"../../dist\""
2627
},
2728
"publishConfig": {
2829
"access": "public"

packages/components/src/__tests__/index.browser.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ describe('export', () => {
1414
StepperDecreaseButton: expect.any(Function),
1515
StepperIncreaseButton: expect.any(Function),
1616
StepperInput: expect.any(Function),
17-
useSelect: expect.any(Function),
1817
useStepper: expect.any(Function),
18+
SelectContext: expect.any(Object),
19+
useSelect: expect.any(Function),
1920
})
2021
})
2122
})

packages/components/src/components/Select/Select.stories.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,23 @@ export const ControlledCheckboxStory: Story = {
4343
render: () => <ControlledCheckbox />,
4444
}
4545

46+
export const SelectWithOptionsStory: Story = {
47+
args: {},
48+
render: () => <SelectWithOptions />,
49+
}
50+
4651
export default meta
4752

4853
function DefaultComponent(
4954
props: Omit<ComponentProps<typeof Select>, 'children'>,
5055
) {
5156
return (
5257
<Select {...props} defaultValue={['Option 1']} onValueChange={() => {}}>
53-
<SelectTrigger>Select</SelectTrigger>
54-
<SelectContainer>
58+
<SelectTrigger>Select2</SelectTrigger>
59+
<SelectContainer
60+
// x={10}
61+
// y={10}
62+
>
5563
<SelectOption disabled value="Option 1">
5664
Option 1
5765
</SelectOption>
@@ -69,13 +77,7 @@ function DefaultComponent(
6977
</Flex>
7078
</SelectOption>
7179
</SelectTrigger>
72-
<SelectContainer
73-
className={css({
74-
right: '0',
75-
top: '0',
76-
transform: 'translateX(100%)',
77-
})}
78-
>
80+
<SelectContainer>
7981
<SelectOption value="Option 6">Option 6</SelectOption>
8082
<SelectOption value="Option 7">Option 7</SelectOption>
8183
</SelectContainer>
@@ -200,3 +202,25 @@ function ControlledRadio() {
200202
</Select>
201203
)
202204
}
205+
206+
function SelectWithOptions() {
207+
return (
208+
<>
209+
<Select
210+
options={[
211+
{ label: 'Option 1', value: 'Option 1' },
212+
{ label: 'Option 2', value: 'Option 2', disabled: true },
213+
{
214+
label: 'Option 3',
215+
value: 'Option 3',
216+
onClick: () => {
217+
console.log('Option 3')
218+
},
219+
},
220+
]}
221+
>
222+
title
223+
</Select>
224+
</>
225+
)
226+
}

packages/components/src/components/Select/__tests__/__snapshots__/index.browser.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`Select > should render 1`] = `
44
<div>
55
<div
6-
class="display-0-inline-block--1 position-0-relative--1 box-sizing-0-border-box-1137980104803605758-1 "
6+
class="display-0-inline-block--1 height-0-fit-content--1 box-sizing-0-border-box-1137980104803605758-1 "
77
>
88
<button
99
aria-expanded="false"

packages/components/src/components/Select/__tests__/index.browser.test.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('Select', () => {
9494
it('should call onValueChange function when it is provided', () => {
9595
const onValueChange = vi.fn()
9696
const { container } = render(
97-
<Select onValueChange={onValueChange} type="radio">
97+
<Select onChange={onValueChange} type="radio">
9898
{children}
9999
</Select>,
100100
)
@@ -371,4 +371,16 @@ describe('Select', () => {
371371
const svg = container.querySelector('svg')
372372
expect(svg).not.toBeInTheDocument()
373373
})
374+
375+
it('should render with options properties', () => {
376+
const { container } = render(
377+
<Select options={[{ label: 'Option 1', value: 'Option 1' }]}>
378+
Select
379+
</Select>,
380+
)
381+
const selectToggle = container.querySelector('[aria-label="Select toggle"]')
382+
fireEvent.click(selectToggle!)
383+
const option1 = container.querySelector('[data-value="Option 1"]')
384+
expect(option1).toBeInTheDocument()
385+
})
374386
})

packages/components/src/components/Select/index.tsx

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,23 @@ import clsx from 'clsx'
55
import {
66
Children,
77
ComponentProps,
8-
createContext,
98
JSX,
109
JSXElementConstructor,
1110
ReactElement,
12-
useContext,
1311
useEffect,
1412
useRef,
1513
useState,
1614
} from 'react'
1715

16+
import { SelectContext, useSelect } from '../../contexts/useSelect'
17+
import { SelectType, SelectValue } from '../../types/select'
1818
import { Button } from '../Button'
1919
import { IconCheck } from './IconCheck'
2020

21-
type SelectType = 'default' | 'radio' | 'checkbox'
22-
type SelectValue<T extends SelectType> = T extends 'radio' ? string : string[]
23-
24-
const SelectContext = createContext<{
25-
open: boolean
26-
setOpen: (open: boolean) => void
27-
value: SelectValue<SelectType>
28-
setValue: (value: string) => void
29-
type: SelectType
30-
} | null>(null)
31-
32-
export const useSelect = () => {
33-
const context = useContext(SelectContext)
34-
if (!context) {
35-
throw new Error('useSelect must be used within a Select')
36-
}
37-
return context
38-
}
39-
40-
interface SelectProps extends ComponentProps<'div'> {
21+
interface SelectProps extends Omit<ComponentProps<'div'>, 'onChange'> {
4122
defaultValue?: SelectValue<SelectType>
4223
value?: SelectValue<SelectType>
43-
onValueChange?: (value: string) => void
24+
onChange?: (value: string) => void
4425
defaultOpen?: boolean
4526
open?: boolean
4627
onOpenChange?: (open: boolean) => void
@@ -58,19 +39,30 @@ interface SelectProps extends ComponentProps<'div'> {
5839
primaryBg?: string
5940
}
6041
typography?: keyof DevupThemeTypography
42+
options?: {
43+
label?: string
44+
disabled?: boolean
45+
onClick?: (
46+
value: string | undefined,
47+
e?: React.MouseEvent<HTMLDivElement>,
48+
) => void
49+
showCheck?: boolean
50+
value: string
51+
}[]
6152
}
6253

6354
export function Select({
6455
type = 'default',
6556
children,
6657
defaultValue,
6758
value: valueProp,
68-
onValueChange,
59+
onChange,
6960
defaultOpen,
7061
open: openProp,
7162
onOpenChange,
7263
colors,
7364
typography,
65+
options,
7466
...props
7567
}: SelectProps) {
7668
const ref = useRef<HTMLDivElement>(null)
@@ -94,7 +86,7 @@ export function Select({
9486
}
9587

9688
const handleValueChange = (nextValue: string) => {
97-
onValueChange?.(nextValue)
89+
onChange?.(nextValue)
9890

9991
if (type === 'default') return
10092
if (type === 'radio') {
@@ -116,12 +108,13 @@ export function Select({
116108
value: valueProp ?? value,
117109
setValue: handleValueChange,
118110
type,
111+
ref,
119112
}}
120113
>
121114
<Box
122115
ref={ref}
123116
display="inline-block"
124-
pos="relative"
117+
h="fit-content"
125118
selectors={{
126119
'&, & *': {
127120
boxSizing: 'border-box',
@@ -142,7 +135,20 @@ export function Select({
142135
typography={typography}
143136
{...props}
144137
>
145-
{children}
138+
{options ? (
139+
<>
140+
<SelectTrigger>{children}</SelectTrigger>
141+
<SelectContainer>
142+
{options?.map((option) => (
143+
<SelectOption {...option} key={'option-' + option.value}>
144+
{option.label ?? option.value}
145+
</SelectOption>
146+
))}
147+
</SelectContainer>
148+
</>
149+
) : (
150+
children
151+
)}
146152
</Box>
147153
</SelectContext.Provider>
148154
)
@@ -206,25 +212,53 @@ export function SelectContainer({
206212
confirmButtonText = '완료',
207213
...props
208214
}: SelectContainerProps) {
209-
const { open, setOpen, type } = useSelect()
215+
const { open, setOpen, type, ref } = useSelect()
210216

211217
if (!open) return null
212218
return (
213219
<VStack
220+
ref={(el) => {
221+
if (!ref.current || !el) return
222+
const combobox = ref.current
223+
224+
// 요소가 움직일 때마다(스크롤, 리사이즈 등) 위치를 갱신하도록 이벤트를 등록합니다.
225+
const updatePosition = () => {
226+
const { height, x, y } = combobox.getBoundingClientRect()
227+
228+
if (el.offsetHeight + y > window.innerHeight)
229+
el.style.bottom = `${window.innerHeight - y + 10}px`
230+
else el.style.top = `${y + height + 10}px`
231+
if (el.offsetWidth + x > window.innerWidth)
232+
el.style.left = `${x - el.offsetWidth + combobox.offsetWidth}px`
233+
else el.style.left = `${x}px`
234+
}
235+
236+
// 최초 위치 설정
237+
updatePosition()
238+
239+
// 스크롤, 리사이즈, DOM 변경 등 요소 위치가 변할 수 있는 이벤트에 리스너 등록
240+
window.addEventListener('scroll', updatePosition, true)
241+
window.addEventListener('resize', updatePosition)
242+
243+
// 컴포넌트 언마운트 시 이벤트 해제
244+
return () => {
245+
window.removeEventListener('scroll', updatePosition, true)
246+
window.removeEventListener('resize', updatePosition)
247+
}
248+
}}
214249
aria-label="Select container"
215250
bg="var(--inputBg, light-dark(#FFF,#2E2E2E))"
216251
border="1px solid var(--border, light-dark(#E4E4E4,#434343))"
217252
borderRadius="8px"
218253
bottom="-4px"
219254
boxShadow="0 2px 2px 0 var(--base10, light-dark(#0000001A,#FFFFFF1A))"
255+
boxSize="fit-content"
220256
gap="6px"
221-
h="fit-content"
257+
minW="232px"
222258
p="10px"
223-
pos="absolute"
259+
pos="fixed"
224260
styleOrder={1}
225-
transform="translateY(100%)"
226261
userSelect="none"
227-
w="232px"
228262
zIndex={1}
229263
{...props}
230264
>
@@ -255,12 +289,12 @@ export function SelectContainer({
255289
}
256290

257291
interface SelectOptionProps extends Omit<ComponentProps<'div'>, 'onClick'> {
292+
value?: string
293+
disabled?: boolean
258294
onClick?: (
259295
value: string | undefined,
260296
e?: React.MouseEvent<HTMLDivElement>,
261297
) => void
262-
disabled?: boolean
263-
value?: string
264298
showCheck?: boolean
265299
}
266300

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createContext, useContext } from 'react'
2+
3+
type SelectType = 'default' | 'radio' | 'checkbox'
4+
type SelectValue<T extends SelectType> = T extends 'radio' ? string : string[]
5+
6+
export const SelectContext = createContext<{
7+
open: boolean
8+
setOpen: (open: boolean) => void
9+
value: SelectValue<SelectType>
10+
setValue: (value: string) => void
11+
type: SelectType
12+
ref: React.RefObject<HTMLDivElement | null>
13+
} | null>(null)
14+
15+
export const useSelect = () => {
16+
const context = useContext(SelectContext)
17+
if (!context) {
18+
throw new Error('useSelect must be used within a Select')
19+
}
20+
return context
21+
}

packages/components/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export {
66
SelectDivider,
77
SelectOption,
88
SelectTrigger,
9-
useSelect,
109
} from './components/Select'
1110
export {
1211
Stepper,
@@ -16,3 +15,5 @@ export {
1615
StepperInput,
1716
useStepper,
1817
} from './components/Stepper'
18+
export { SelectContext, useSelect } from './contexts/useSelect'
19+
export type { SelectType, SelectValue } from './types/select'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type SelectType = 'default' | 'radio' | 'checkbox'
2+
export type SelectValue<T extends SelectType> = T extends 'radio'
3+
? string
4+
: string[]

0 commit comments

Comments
 (0)