@@ -5,42 +5,23 @@ import clsx from 'clsx'
55import {
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'
1818import { Button } from '../Button'
1919import { 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
6354export 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
257291interface 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
0 commit comments