@@ -47,7 +47,12 @@ interface MultiSelectProps
4747 searchTerm : string ,
4848 ) => boolean ;
4949
50+ popoverContentClassName ?: string ;
51+ customTrigger ?: React . ReactNode ;
5052 renderOption ?: ( option : { value : string ; label : string } ) => React . ReactNode ;
53+ align ?: "center" | "start" | "end" ;
54+ side ?: "left" | "right" | "top" | "bottom" ;
55+ showSelectedValuesInModal ?: boolean ;
5156}
5257
5358export const MultiSelect = forwardRef < HTMLButtonElement , MultiSelectProps > (
@@ -62,6 +67,8 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
6267 overrideSearchFn,
6368 renderOption,
6469 searchPlaceholder,
70+ popoverContentClassName,
71+ showSelectedValuesInModal = false ,
6572 ...props
6673 } ,
6774 ref ,
@@ -155,111 +162,100 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
155162 return (
156163 < Popover open = { isPopoverOpen } onOpenChange = { setIsPopoverOpen } modal >
157164 < 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 >
185199
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"
191203 />
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" />
208205 </ 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 >
214212 < ChevronDown className = "h-4 cursor-pointer text-muted-foreground" />
215213 </ 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+ ) }
226217 </ PopoverTrigger >
227218 < 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 }
230225 sideOffset = { 10 }
231226 onEscapeKeyDown = { ( ) => setIsPopoverOpen ( false ) }
232227 style = { {
233228 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)" ,
235231 } }
236232 ref = { popoverElRef }
237233 >
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
249249 </ div >
250+ ) }
250251
252+ { optionsToShow . length > 0 && (
251253 < 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"
254256 >
255257 { /* List */ }
256258 < div >
257- { optionsToShow . length === 0 && (
258- < div className = "flex justify-center py-10" >
259- No results found
260- </ div >
261- ) }
262-
263259 { optionsToShow . map ( ( option , i ) => {
264260 const isSelected = selectedValues . includes ( option . value ) ;
265261 return (
@@ -293,7 +289,20 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
293289 } ) }
294290 </ div >
295291 </ 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+ ) }
297306 </ PopoverContent >
298307 </ Popover >
299308 ) ;
@@ -319,3 +328,44 @@ function ClosableBadge(props: {
319328}
320329
321330MultiSelect . 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