@@ -10,7 +10,8 @@ import {
1010} from "@humansignal/shad/components/ui/command" ;
1111import { Popover , PopoverContent , PopoverTrigger } from "@humansignal/shad/components/ui/popover" ;
1212import type { SelectOption , OptionProps , SelectProps } from "./types.ts" ;
13- import { Checkbox , Label } from "@humansignal/ui" ;
13+ import { Checkbox , Label , Typography } from "@humansignal/ui" ;
14+ import { Badge } from "../badge/badge" ;
1415import { isDefined } from "@humansignal/core/lib/utils/helpers" ;
1516import { IconChevron , IconChevronDown } from "@humansignal/icons" ;
1617import clsx from "clsx" ;
@@ -22,6 +23,113 @@ import InfiniteLoader from "react-window-infinite-loader";
2223const VARIABLE_LIST_ITEM_HEIGHT = 40 ;
2324const VARIABLE_LIST_COUNT_RENDERED = 5 ;
2425const VARIABLE_LIST_PAGE_SIZE = 20 ;
26+
27+ /**
28+ * Props for SelectedItemsGroup component
29+ */
30+ type SelectedItemsGroupProps = {
31+ expanded : boolean ;
32+ onToggleExpand : ( ) => void ;
33+ selectedOptions : any [ ] ;
34+ onDeselectItem : ( value : any ) => void ;
35+ onDeselectAll : ( ) => void ;
36+ disabled ?: boolean ;
37+ } ;
38+
39+ /**
40+ * SelectedItemsGroup - Internal component for displaying selected items in a collapsible group
41+ * Only visible when multiple, searchable, and isVirtualList are all true
42+ */
43+ const SelectedItemsGroup = ( {
44+ expanded,
45+ onToggleExpand,
46+ selectedOptions,
47+ onDeselectItem,
48+ onDeselectAll,
49+ disabled,
50+ } : SelectedItemsGroupProps ) => {
51+ const handleItemClick = useCallback (
52+ ( option : any ) => {
53+ if ( disabled ) return ;
54+ const value = option ?. value ?? option ;
55+ onDeselectItem ( value ) ;
56+ } ,
57+ [ onDeselectItem , disabled ] ,
58+ ) ;
59+
60+ const handleDeselectAllClick = useCallback (
61+ ( e : React . MouseEvent ) => {
62+ e . stopPropagation ( ) ;
63+ if ( disabled ) return ;
64+ onDeselectAll ( ) ;
65+ } ,
66+ [ onDeselectAll , disabled ] ,
67+ ) ;
68+
69+ return (
70+ < div className = { styles . selectedItemsGroup } >
71+ { /* Header - Always visible */ }
72+ < button
73+ type = "button"
74+ className = { styles . selectedItemsHeader }
75+ onClick = { onToggleExpand }
76+ aria-expanded = { expanded }
77+ aria-label = { `Selected items group, ${ selectedOptions . length } items selected` }
78+ >
79+ { /* Caret icon */ }
80+ { expanded ? (
81+ < IconChevron className = { styles . selectedItemsCaret } aria-hidden = "true" />
82+ ) : (
83+ < IconChevronDown className = { styles . selectedItemsCaret } aria-hidden = "true" />
84+ ) }
85+
86+ { /* Deselect all checkbox */ }
87+ < Checkbox
88+ tabIndex = { - 1 }
89+ checked = { true }
90+ readOnly
91+ disabled = { disabled }
92+ onClick = { handleDeselectAllClick }
93+ aria-label = "Deselect all items"
94+ />
95+
96+ { /* Title with counter badge */ }
97+ < div className = { styles . selectedItemsTitle } >
98+ < Typography variant = "body" > Selected items</ Typography >
99+ < Badge variant = "info" shape = "squared" className = "ml-auto" >
100+ { selectedOptions . length }
101+ </ Badge >
102+ </ div >
103+ </ button >
104+
105+ { /* Content - Conditionally rendered when expanded */ }
106+ { expanded && (
107+ < div className = { styles . selectedItemsContent } >
108+ { selectedOptions . map ( ( option , index ) => {
109+ const optionValue = option ?. value ?? option ;
110+ const label = option ?. label ?? optionValue ;
111+
112+ return (
113+ < button
114+ key = { `selected-${ optionValue } -${ index } ` }
115+ type = "button"
116+ className = { styles . selectedItem }
117+ onClick = { ( ) => handleItemClick ( option ) }
118+ tabIndex = { disabled ? - 1 : 0 }
119+ aria-label = { `Deselect ${ label } ` }
120+ disabled = { disabled }
121+ >
122+ < Checkbox tabIndex = { - 1 } checked = { true } readOnly disabled = { disabled } />
123+ < div className = "w-full min-w-0 truncate" > { label } </ div >
124+ </ button >
125+ ) ;
126+ } ) }
127+ </ div >
128+ ) }
129+ </ div >
130+ ) ;
131+ } ;
132+
25133/*
26134 * This file defines a custom Select component for the Design System, which uses a fully custom UI for
27135 * dropdowns and options.
@@ -109,6 +217,7 @@ export const Select = forwardRef(
109217 initialValue = initialValue [ 0 ] ;
110218 }
111219 const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
220+ const [ selectedGroupExpanded , setSelectedGroupExpanded ] = useState < boolean > ( false ) ;
112221 const [ value , setValue ] = useState < any > ( initialValue ) ;
113222
114223 valueRef . current = value ;
@@ -184,6 +293,13 @@ export const Select = forwardRef(
184293 } , [ options ] ) ;
185294
186295 const _options = useMemo ( ( ) => {
296+ // If searchFilter is provided, always use it (even with empty query)
297+ // This allows custom filtering logic for API-based searches
298+ if ( searchFilter ) {
299+ return flatOptions . filter ( ( option ) => searchFilter ( option , query ?? "" ) ) ;
300+ }
301+
302+ // Default behavior: no filtering when not searchable or query is empty
187303 if ( ! searchable || ! query . trim ( ) ) return options ;
188304
189305 const filterHandler = ( option : any , queryString : string ) => {
@@ -194,7 +310,7 @@ export const Select = forwardRef(
194310 value ?. toString ( ) ?. toLowerCase ( ) . includes ( queryString . toLowerCase ( ) )
195311 ) ;
196312 } ;
197- return flatOptions . filter ( ( option ) => ( searchFilter ?? filterHandler ) ( option , query ) ) ;
313+ return flatOptions . filter ( ( option ) => filterHandler ( option , query ) ) ;
198314 } , [ options , flatOptions , searchable , query , searchFilter ] ) ;
199315
200316 const isSelected = useCallback (
@@ -390,11 +506,30 @@ export const Select = forwardRef(
390506 ) }
391507 < CommandList
392508 label = "Select an option"
393- className = {
394- searchable ? "shadow-inner shadow-neutral-surface-inset border-t border-neutral-border shadow-" : ""
395- }
509+ className = { cnm ( {
510+ "shadow-inner shadow-neutral-surface-inset border-t border-neutral-border shadow-" : searchable ,
511+ "max-h-none" : footer !== undefined ,
512+ } ) }
396513 >
514+ { /* Selected Items Group - Only for multiple + searchable + virtual lists */ }
515+ { multiple && searchable && isVirtualList && selectedOptions . length > 0 && (
516+ < SelectedItemsGroup
517+ expanded = { selectedGroupExpanded }
518+ onToggleExpand = { ( ) => setSelectedGroupExpanded ( ! selectedGroupExpanded ) }
519+ selectedOptions = { selectedOptions }
520+ onDeselectItem = { ( value ) => _onChange ( value , true ) }
521+ onDeselectAll = { ( ) => {
522+ selectedOptions . forEach ( ( opt ) => {
523+ const val = opt ?. value ?? opt ;
524+ _onChange ( val , true ) ;
525+ } ) ;
526+ } }
527+ disabled = { disabled }
528+ />
529+ ) }
530+
397531 < CommandEmpty > { searchable ? "No results found." : "" } </ CommandEmpty >
532+
398533 < CommandGroup >
399534 { props . header ? props . header : null }
400535 { isVirtualList ? (
0 commit comments