@@ -47,7 +47,12 @@ interface MultiSelectProps
47
47
searchTerm : string ,
48
48
) => boolean ;
49
49
50
+ popoverContentClassName ?: string ;
51
+ customTrigger ?: React . ReactNode ;
50
52
renderOption ?: ( option : { value : string ; label : string } ) => React . ReactNode ;
53
+ align ?: "center" | "start" | "end" ;
54
+ side ?: "left" | "right" | "top" | "bottom" ;
55
+ showSelectedValuesInModal ?: boolean ;
51
56
}
52
57
53
58
export const MultiSelect = forwardRef < HTMLButtonElement , MultiSelectProps > (
@@ -62,6 +67,8 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
62
67
overrideSearchFn,
63
68
renderOption,
64
69
searchPlaceholder,
70
+ popoverContentClassName,
71
+ showSelectedValuesInModal = false ,
65
72
...props
66
73
} ,
67
74
ref ,
@@ -155,111 +162,100 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
155
162
return (
156
163
< Popover open = { isPopoverOpen } onOpenChange = { setIsPopoverOpen } modal >
157
164
< PopoverTrigger asChild >
158
- < Button
159
- ref = { ref }
160
- { ...props }
161
- onClick = { handleTogglePopover }
162
- className = { cn (
163
- "flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit" ,
164
- className ,
165
- ) }
166
- >
167
- { selectedValues . length > 0 ? (
168
- < div className = "flex w-full justify-between" >
169
- { /* badges */ }
170
- < div className = "flex flex-wrap items-center gap-1.5" >
171
- { selectedValues . slice ( 0 , maxCount ) . map ( ( value ) => {
172
- const option = options . find ( ( o ) => o . value === value ) ;
173
- if ( ! option ) {
174
- return null ;
175
- }
176
-
177
- return (
178
- < ClosableBadge
179
- key = { value }
180
- label = { option . label }
181
- onClose = { ( ) => toggleOption ( value ) }
182
- />
183
- ) ;
184
- } ) }
165
+ { props . customTrigger || (
166
+ < Button
167
+ ref = { ref }
168
+ { ...props }
169
+ onClick = { handleTogglePopover }
170
+ className = { cn (
171
+ "flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit" ,
172
+ className ,
173
+ ) }
174
+ >
175
+ { selectedValues . length > 0 ? (
176
+ < div className = "flex w-full items-center justify-between" >
177
+ < SelectedChainsBadges
178
+ selectedValues = { selectedValues }
179
+ options = { options }
180
+ maxCount = { maxCount }
181
+ onClose = { handleClear }
182
+ toggleOption = { toggleOption }
183
+ clearExtraOptions = { clearExtraOptions }
184
+ />
185
+ < div className = "flex items-center justify-between gap-2" >
186
+ { /* Clear All */ }
187
+ { /* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */ }
188
+ { /* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */ }
189
+ < div
190
+ role = "button"
191
+ onClick = { ( event ) => {
192
+ event . stopPropagation ( ) ;
193
+ handleClear ( ) ;
194
+ } }
195
+ className = "rounded p-1 hover:bg-accent"
196
+ >
197
+ < XIcon className = "h-4 cursor-pointer text-muted-foreground" />
198
+ </ div >
185
199
186
- { /* +X more */ }
187
- { selectedValues . length > maxCount && (
188
- < ClosableBadge
189
- label = { `+ ${ selectedValues . length - maxCount } more` }
190
- onClose = { clearExtraOptions }
200
+ < Separator
201
+ orientation = "vertical"
202
+ className = "flex h-full min-h-6"
191
203
/>
192
- ) }
193
- </ div >
194
-
195
- < div className = "flex items-center justify-between gap-2" >
196
- { /* Clear All */ }
197
- { /* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */ }
198
- { /* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */ }
199
- < div
200
- role = "button"
201
- onClick = { ( event ) => {
202
- event . stopPropagation ( ) ;
203
- handleClear ( ) ;
204
- } }
205
- className = "rounded p-1 hover:bg-accent"
206
- >
207
- < XIcon className = "h-4 cursor-pointer text-muted-foreground" />
204
+ < ChevronDown className = "h-4 cursor-pointer text-muted-foreground" />
208
205
</ div >
209
-
210
- < Separator
211
- orientation = "vertical"
212
- className = "flex h-full min-h-6"
213
- />
206
+ </ div >
207
+ ) : (
208
+ < div className = "flex w-full items-center justify-between" >
209
+ < span className = "text-muted-foreground text-sm" >
210
+ { placeholder }
211
+ </ span >
214
212
< ChevronDown className = "h-4 cursor-pointer text-muted-foreground" />
215
213
</ div >
216
- </ div >
217
- ) : (
218
- < div className = "flex w-full items-center justify-between" >
219
- < span className = "text-muted-foreground text-sm" >
220
- { placeholder }
221
- </ span >
222
- < ChevronDown className = "h-4 cursor-pointer text-muted-foreground" />
223
- </ div >
224
- ) }
225
- </ Button >
214
+ ) }
215
+ </ Button >
216
+ ) }
226
217
</ PopoverTrigger >
227
218
< PopoverContent
228
- className = "p-0"
229
- align = "center"
219
+ className = { cn (
220
+ "flex max-h-[60vh] flex-col p-0" ,
221
+ popoverContentClassName ,
222
+ ) }
223
+ align = { props . align }
224
+ side = { props . side }
230
225
sideOffset = { 10 }
231
226
onEscapeKeyDown = { ( ) => setIsPopoverOpen ( false ) }
232
227
style = { {
233
228
width : "var(--radix-popover-trigger-width)" ,
234
- maxHeight : "var(--radix-popover-content-available-height)" ,
229
+ height :
230
+ "calc(var(--radix-popover-content-available-height) - 40px)" ,
235
231
} }
236
232
ref = { popoverElRef }
237
233
>
238
- < div >
239
- { /* Search */ }
240
- < div className = "relative" >
241
- < Input
242
- placeholder = { searchPlaceholder || "Search" }
243
- value = { searchValue }
244
- onChange = { ( e ) => setSearchValue ( e . target . value ) }
245
- className = "!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
246
- onKeyDown = { handleInputKeyDown }
247
- />
248
- < SearchIcon className = "-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
234
+ { /* Search */ }
235
+ < div className = "relative" >
236
+ < Input
237
+ placeholder = { searchPlaceholder || "Search" }
238
+ value = { searchValue }
239
+ onChange = { ( e ) => setSearchValue ( e . target . value ) }
240
+ className = "!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
241
+ onKeyDown = { handleInputKeyDown }
242
+ />
243
+ < SearchIcon className = "-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
244
+ </ div >
245
+
246
+ { optionsToShow . length === 0 && (
247
+ < div className = "flex flex-1 flex-col items-center justify-center py-10 text-sm" >
248
+ No results found
249
249
</ div >
250
+ ) }
250
251
252
+ { optionsToShow . length > 0 && (
251
253
< ScrollShadow
252
- scrollableClassName = "max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
253
- className = "rounded"
254
+ scrollableClassName = "p-1 h-full "
255
+ className = "flex-1 rounded"
254
256
>
255
257
{ /* List */ }
256
258
< div >
257
- { optionsToShow . length === 0 && (
258
- < div className = "flex justify-center py-10" >
259
- No results found
260
- </ div >
261
- ) }
262
-
263
259
{ optionsToShow . map ( ( option , i ) => {
264
260
const isSelected = selectedValues . includes ( option . value ) ;
265
261
return (
@@ -293,7 +289,20 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
293
289
} ) }
294
290
</ div >
295
291
</ ScrollShadow >
296
- </ div >
292
+ ) }
293
+
294
+ { showSelectedValuesInModal && selectedValues . length > 0 && (
295
+ < div className = "border-t px-3 py-3" >
296
+ < SelectedChainsBadges
297
+ selectedValues = { selectedValues }
298
+ options = { options }
299
+ maxCount = { maxCount }
300
+ onClose = { handleClear }
301
+ toggleOption = { toggleOption }
302
+ clearExtraOptions = { clearExtraOptions }
303
+ />
304
+ </ div >
305
+ ) }
297
306
</ PopoverContent >
298
307
</ Popover >
299
308
) ;
@@ -319,3 +328,44 @@ function ClosableBadge(props: {
319
328
}
320
329
321
330
MultiSelect . displayName = "MultiSelect" ;
331
+
332
+ function SelectedChainsBadges ( props : {
333
+ selectedValues : string [ ] ;
334
+ options : {
335
+ label : string ;
336
+ value : string ;
337
+ } [ ] ;
338
+ maxCount : number ;
339
+ onClose : ( ) => void ;
340
+ toggleOption : ( value : string ) => void ;
341
+ clearExtraOptions : ( ) => void ;
342
+ } ) {
343
+ const { selectedValues, options, maxCount, toggleOption, clearExtraOptions } =
344
+ props ;
345
+ return (
346
+ < div className = "flex flex-wrap items-center gap-1.5" >
347
+ { selectedValues . slice ( 0 , maxCount ) . map ( ( value ) => {
348
+ const option = options . find ( ( o ) => o . value === value ) ;
349
+ if ( ! option ) {
350
+ return null ;
351
+ }
352
+
353
+ return (
354
+ < ClosableBadge
355
+ key = { value }
356
+ label = { option . label }
357
+ onClose = { ( ) => toggleOption ( value ) }
358
+ />
359
+ ) ;
360
+ } ) }
361
+
362
+ { /* +X more */ }
363
+ { selectedValues . length > maxCount && (
364
+ < ClosableBadge
365
+ label = { `+ ${ selectedValues . length - maxCount } more` }
366
+ onClose = { clearExtraOptions }
367
+ />
368
+ ) }
369
+ </ div >
370
+ ) ;
371
+ }
0 commit comments