@@ -34,6 +34,7 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
3434 status,
3535 onChange,
3636 children,
37+ className,
3738 ...attrs
3839} , ref ) => {
3940 const [ expanded , setExpanded ] = useState ( false )
@@ -42,6 +43,7 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
4243 const [ take , setTake ] = useState ( viewCount )
4344 const [ filteredValues , setFilteredValues ] = useState < any [ ] > ( [ ] )
4445 const txtInputRef = useRef < HTMLInputElement > ( null )
46+ const containerRef = useRef < HTMLDivElement > ( null )
4547
4648 const apiState = useApiState ( )
4749 const useLabel = label ?? humanize ( toPascalCase ( id ) )
@@ -57,10 +59,20 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
5759 [ responseStatus , id ]
5860 )
5961
60- const cls = useMemo ( ( ) => [
61- input . base ,
62- errorField ? input . invalid : input . valid
63- ] . join ( ' ' ) , [ errorField ] )
62+ const cls = useMemo ( ( ) => {
63+ if ( multiple ) {
64+ return [
65+ 'w-full cursor-text flex flex-wrap sm:text-sm rounded-md dark:text-white dark:bg-gray-900 border focus-within:border-transparent focus-within:ring-1 focus-within:outline-none' ,
66+ errorField
67+ ? 'pr-10 border-red-300 text-red-900 placeholder-red-300 focus-within:outline-none focus-within:ring-red-500 focus-within:border-red-500'
68+ : 'shadow-sm border-gray-300 dark:border-gray-600 focus-within:ring-indigo-500 focus-within:border-indigo-500'
69+ ] . join ( ' ' )
70+ }
71+ return [
72+ input . base ,
73+ errorField ? input . invalid : input . valid
74+ ] . join ( ' ' )
75+ } , [ errorField , multiple ] )
6476
6577 const filteredOptions = useMemo ( ( ) => {
6678 let ret = ! inputValue
@@ -234,24 +246,53 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
234246 refresh ( )
235247 } , [ ] )
236248
249+ // Close dropdown when clicking outside
250+ useEffect ( ( ) => {
251+ const handleClickOutside = ( event : MouseEvent ) => {
252+ if ( containerRef . current && ! containerRef . current . contains ( event . target as Node ) ) {
253+ setExpanded ( false )
254+ }
255+ }
256+
257+ if ( expanded ) {
258+ document . addEventListener ( 'mousedown' , handleClickOutside )
259+ return ( ) => {
260+ document . removeEventListener ( 'mousedown' , handleClickOutside )
261+ }
262+ }
263+ } , [ expanded ] )
264+
237265 const hasOption = useCallback ( ( option : any ) => {
238- return Array . isArray ( value ) && value . indexOf ( option ) >= 0
266+ if ( ! Array . isArray ( value ) ) return false
267+
268+ // For objects, compare by key property if it exists
269+ if ( option && typeof option === 'object' && 'key' in option ) {
270+ return value . some ( v => v && typeof v === 'object' && 'key' in v && v . key === option . key )
271+ }
272+
273+ // For primitives, use indexOf
274+ return value . indexOf ( option ) >= 0
239275 } , [ value ] )
240276
241277 const select = useCallback ( ( option : any ) => {
242- setInputValue ( '' )
243- setExpanded ( false )
244-
245278 if ( multiple ) {
246279 let newValues = Array . from ( value || [ ] )
247280 if ( hasOption ( option ) ) {
248- newValues = newValues . filter ( x => x !== option )
281+ // Remove the option - use proper comparison for objects
282+ if ( option && typeof option === 'object' && 'key' in option ) {
283+ newValues = newValues . filter ( x => ! ( x && typeof x === 'object' && 'key' in x && x . key === option . key ) )
284+ } else {
285+ newValues = newValues . filter ( x => x !== option )
286+ }
249287 } else {
250288 newValues . push ( option )
251289 }
252290 setActive ( null )
253291 onChange ?.( newValues )
292+ // Don't clear input or close dropdown in multiple mode - keep it open for more selections
254293 } else {
294+ setInputValue ( '' )
295+ setExpanded ( false )
255296 let val = option
256297 onChange ?.( val )
257298 }
@@ -274,35 +315,86 @@ const Autocomplete = forwardRef<AutocompleteRef, AutocompleteProps & Omit<React.
274315 } , [ children ] )
275316
276317 return (
277- < div id = { `${ id } -autocomplete` } >
318+ < div id = { `${ id } -autocomplete` } className = { className } ref = { containerRef } >
278319 { useLabel && (
279320 < label htmlFor = { `${ id } -text` } className = "block text-sm font-medium text-gray-700 dark:text-gray-300" >
280321 { useLabel }
281322 </ label >
282323 ) }
283324
284325 < div className = "relative mt-1" >
285- < input
286- ref = { txtInputRef }
287- id = { `${ id } -text` }
288- type = "text"
289- role = "combobox"
290- aria-controls = "options"
291- aria-expanded = "false"
292- autoComplete = "off"
293- spellCheck = "false"
294- value = { inputValue }
295- onChange = { ( e ) => setInputValue ( e . target . value ) }
296- className = { cls }
297- placeholder = { multiple || ! value ? placeholder : '' }
298- readOnly = { ! multiple && ! ! value && ! expanded }
299- onKeyDown = { keyDown }
300- onKeyUp = { keyUp }
301- onClick = { onInputClick }
302- onPaste = { onPaste }
303- required = { false }
304- { ...attrs }
305- />
326+ { multiple ? (
327+ < div className = { cls } onClick = { onInputClick } tabIndex = { - 1 } >
328+ < div className = "flex flex-wrap pb-1.5" >
329+ { Array . isArray ( value ) && value . map ( ( option , idx ) => (
330+ < div key = { idx } className = "pt-1.5 pl-1" >
331+ < span className = "inline-flex rounded-full items-center py-0.5 pl-2.5 pr-1 text-sm font-medium bg-indigo-100 dark:bg-indigo-800 text-indigo-700 dark:text-indigo-300" >
332+ { renderItem ( option ) }
333+ < button
334+ type = "button"
335+ onClick = { ( e ) => {
336+ e . stopPropagation ( )
337+ if ( Array . isArray ( value ) ) {
338+ onChange ?.( value . filter ( v => v !== option ) )
339+ }
340+ } }
341+ className = "flex-shrink-0 ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center text-indigo-400 dark:text-indigo-500 hover:bg-indigo-200 dark:hover:bg-indigo-800 hover:text-indigo-500 dark:hover:text-indigo-400 focus:outline-none focus:bg-indigo-500 focus:text-white dark:focus:text-black"
342+ >
343+ < svg className = "h-2 w-2" stroke = "currentColor" fill = "none" viewBox = "0 0 8 8" >
344+ < path strokeLinecap = "round" strokeWidth = "1.5" d = "M1 1l6 6m0-6L1 7" />
345+ </ svg >
346+ </ button >
347+ </ span >
348+ </ div >
349+ ) ) }
350+ < div className = "pt-1.5 pl-1 shrink" >
351+ < input
352+ ref = { txtInputRef }
353+ id = { `${ id } -text` }
354+ type = "text"
355+ role = "combobox"
356+ aria-controls = "options"
357+ aria-expanded = "false"
358+ autoComplete = "off"
359+ spellCheck = "false"
360+ className = "p-0 dark:bg-transparent rounded-md border-none focus:!border-none focus:!outline-none"
361+ style = { { boxShadow : 'none !important' , width : `${ inputValue . length + 1 } ch` } }
362+ value = { inputValue }
363+ onChange = { ( e ) => setInputValue ( e . target . value ) }
364+ placeholder = { ! value || ( Array . isArray ( value ) && value . length === 0 ) ? placeholder : '' }
365+ onKeyDown = { keyDown }
366+ onKeyUp = { keyUp }
367+ onClick = { onInputClick }
368+ onPaste = { onPaste }
369+ required = { false }
370+ { ...attrs }
371+ />
372+ </ div >
373+ </ div >
374+ </ div >
375+ ) : (
376+ < input
377+ ref = { txtInputRef }
378+ id = { `${ id } -text` }
379+ type = "text"
380+ role = "combobox"
381+ aria-controls = "options"
382+ aria-expanded = "false"
383+ autoComplete = "off"
384+ spellCheck = "false"
385+ value = { inputValue }
386+ onChange = { ( e ) => setInputValue ( e . target . value ) }
387+ className = { cls }
388+ placeholder = { multiple || ! value ? placeholder : '' }
389+ readOnly = { ! multiple && ! ! value && ! expanded }
390+ onKeyDown = { keyDown }
391+ onKeyUp = { keyUp }
392+ onClick = { onInputClick }
393+ onPaste = { onPaste }
394+ required = { false }
395+ { ...attrs }
396+ />
397+ ) }
306398
307399 < button
308400 type = "button"
0 commit comments