@@ -2,17 +2,21 @@ import { useState, useRef, useEffect } from 'react';
22import { Badge } from '@/components/ui/badge' ;
33import { Button } from '@/components/ui/button' ;
44import { Input } from '@/components/ui/input' ;
5- import { TagMultiSelectProps } from '@/components/utils/types' ;
6- import { ChevronDown , Plus } from 'lucide-react' ;
5+ import { MultiSelectProps } from '@/components/utils/types' ;
6+ import { ChevronDown , Plus , Check , X } from 'lucide-react' ;
7+ import { getFilteredItems , shouldShowCreateOption } from './multi-select-utils' ;
78
8- export const TagMultiSelect = ( {
9- availableTags ,
10- selectedTags ,
11- onTagsChange ,
12- placeholder = 'Select or create tags ' ,
9+ export const MultiSelect = ( {
10+ availableItems ,
11+ selectedItems ,
12+ onItemsChange ,
13+ placeholder = 'Select or create items ' ,
1314 disabled = false ,
1415 className = '' ,
15- } : TagMultiSelectProps ) => {
16+ showActions = false ,
17+ onSave,
18+ onCancel,
19+ } : MultiSelectProps ) => {
1620 const [ isOpen , setIsOpen ] = useState ( false ) ;
1721 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
1822 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
@@ -33,56 +37,56 @@ export const TagMultiSelect = ({
3337 return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
3438 } , [ ] ) ;
3539
36- const getFilteredTags = ( ) => {
37- return availableTags . filter (
38- ( tag ) =>
39- tag . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) &&
40- ! selectedTags . includes ( tag )
41- ) ;
42- } ;
40+ const filteredItems = getFilteredItems (
41+ availableItems ,
42+ selectedItems ,
43+ searchTerm
44+ ) ;
4345
44- const handleTagSelect = ( tag : string ) => {
45- if ( ! selectedTags . includes ( tag ) ) {
46- onTagsChange ( [ ...selectedTags , tag ] ) ;
46+ const handleItemSelect = ( item : string ) => {
47+ if ( ! selectedItems . includes ( item ) ) {
48+ onItemsChange ( [ ...selectedItems , item ] ) ;
4749 }
4850 setSearchTerm ( '' ) ;
4951 } ;
5052
51- const handleTagRemove = ( tagToRemove : string ) => {
52- onTagsChange ( selectedTags . filter ( ( tag ) => tag !== tagToRemove ) ) ;
53+ const handleItemRemove = ( itemToRemove : string ) => {
54+ onItemsChange ( selectedItems . filter ( ( item ) => item !== itemToRemove ) ) ;
5355 } ;
5456
55- const handleNewTagCreate = ( ) => {
57+ const handleNewItemCreate = ( ) => {
5658 const trimmedTerm = searchTerm . trim ( ) ;
5759 if (
5860 trimmedTerm &&
59- ! selectedTags . includes ( trimmedTerm ) &&
60- ! availableTags . includes ( trimmedTerm )
61+ ! selectedItems . includes ( trimmedTerm ) &&
62+ ! availableItems . includes ( trimmedTerm )
6163 ) {
62- onTagsChange ( [ ...selectedTags , trimmedTerm ] ) ;
64+ onItemsChange ( [ ...selectedItems , trimmedTerm ] ) ;
6365 setSearchTerm ( '' ) ;
6466 }
6567 } ;
6668
6769 const handleKeyDown = ( e : React . KeyboardEvent ) => {
6870 if ( e . key === 'Enter' ) {
6971 e . preventDefault ( ) ;
70- const filteredTags = getFilteredTags ( ) ;
71- if ( filteredTags . length > 0 ) {
72- handleTagSelect ( filteredTags [ 0 ] ) ;
73- } else if ( searchTerm . trim ( ) ) {
74- handleNewTagCreate ( ) ;
72+ if ( searchTerm . trim ( ) ) {
73+ if ( filteredItems . length > 0 ) {
74+ handleItemSelect ( filteredItems [ 0 ] ) ;
75+ } else {
76+ handleNewItemCreate ( ) ;
77+ }
7578 }
7679 } else if ( e . key === 'Escape' ) {
7780 setIsOpen ( false ) ;
7881 setSearchTerm ( '' ) ;
7982 }
8083 } ;
8184
82- const showCreateOption =
83- searchTerm . trim ( ) &&
84- ! availableTags . includes ( searchTerm . trim ( ) ) &&
85- ! selectedTags . includes ( searchTerm . trim ( ) ) ;
85+ const showCreate = shouldShowCreateOption (
86+ searchTerm ,
87+ availableItems ,
88+ selectedItems
89+ ) ;
8690
8791 return (
8892 < div className = { `relative ${ className } ` } ref = { dropdownRef } >
@@ -92,15 +96,59 @@ export const TagMultiSelect = ({
9296 onClick = { ( ) => setIsOpen ( ! isOpen ) }
9397 disabled = { disabled }
9498 className = "w-full justify-between text-left font-normal"
99+ aria-label = "Select items"
95100 >
96101 < span className = "truncate" >
97- { selectedTags . length > 0
98- ? `${ selectedTags . length } tag ${ selectedTags . length > 1 ? 's' : '' } selected`
102+ { selectedItems . length > 0
103+ ? `${ selectedItems . length } item ${ selectedItems . length > 1 ? 's' : '' } selected`
99104 : placeholder }
100105 </ span >
101106 < ChevronDown className = "h-4 w-4 opacity-50" />
102107 </ Button >
103108
109+ { selectedItems . length > 0 && (
110+ < div className = "flex flex-wrap items-center gap-2 mt-2" >
111+ { selectedItems . map ( ( item ) => (
112+ < Badge key = { item } className = "flex items-center gap-1" >
113+ < span > { item } </ span >
114+ < button
115+ type = "button"
116+ onClick = { ( ) => handleItemRemove ( item ) }
117+ className = "ml-1 text-red-500 hover:text-red-700 text-xs"
118+ disabled = { disabled }
119+ aria-label = { `Remove ${ item } ` }
120+ >
121+ ✖
122+ </ button >
123+ </ Badge >
124+ ) ) }
125+ { showActions && onSave && onCancel && (
126+ < div className = "flex items-center gap-1 whitespace-nowrap" >
127+ < Button
128+ type = "button"
129+ variant = "ghost"
130+ size = "icon"
131+ onClick = { onSave }
132+ className = "h-8 w-8"
133+ aria-label = "Save items"
134+ >
135+ < Check className = "h-4 w-4 text-green-500" />
136+ </ Button >
137+ < Button
138+ type = "button"
139+ variant = "ghost"
140+ size = "icon"
141+ onClick = { onCancel }
142+ className = "h-8 w-8"
143+ aria-label = "Cancel"
144+ >
145+ < X className = "h-4 w-4 text-red-500" />
146+ </ Button >
147+ </ div >
148+ ) }
149+ </ div >
150+ ) }
151+
104152 { isOpen && (
105153 < div className = "absolute z-50 w-full mt-1 bg-background border border-border rounded-md shadow-lg max-h-60 overflow-hidden" >
106154 < div className = "p-2 border-b" >
@@ -109,33 +157,27 @@ export const TagMultiSelect = ({
109157 value = { searchTerm }
110158 onChange = { ( e ) => setSearchTerm ( e . target . value ) }
111159 onKeyDown = { handleKeyDown }
112- placeholder = "Search or create tags ..."
160+ placeholder = "Search or create..."
113161 className = "h-8"
114162 autoFocus
115163 />
116164 </ div >
117165
118166 < div className = "max-h-40 overflow-y-auto" >
119- { getFilteredTags ( ) . map ( ( tag ) => (
167+ { filteredItems . map ( ( item ) => (
120168 < div
121- key = { tag }
122- className = "flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
123- onClick = { ( ) => handleTagSelect ( tag ) }
169+ key = { item }
170+ className = "px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
171+ onClick = { ( ) => handleItemSelect ( item ) }
124172 >
125- < input
126- type = "checkbox"
127- checked = { false }
128- readOnly
129- className = "h-4 w-4"
130- />
131- < span className = "flex-1 text-sm" > { tag } </ span >
173+ < span className = "text-sm" > { item } </ span >
132174 </ div >
133175 ) ) }
134176
135- { showCreateOption && (
177+ { showCreate && (
136178 < div
137179 className = "flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-accent transition-colors border-t"
138- onClick = { handleNewTagCreate }
180+ onClick = { handleNewItemCreate }
139181 >
140182 < Plus className = "h-4 w-4 text-muted-foreground" />
141183 < span className = "flex-1 text-sm" >
@@ -144,36 +186,14 @@ export const TagMultiSelect = ({
144186 </ div >
145187 ) }
146188
147- { getFilteredTags ( ) . length === 0 && ! showCreateOption && (
189+ { filteredItems . length === 0 && ! showCreate && (
148190 < div className = "px-3 py-2 text-sm text-muted-foreground text-center" >
149- No tags found
191+ No items found
150192 </ div >
151193 ) }
152194 </ div >
153195 </ div >
154196 ) }
155-
156- { selectedTags . length > 0 && (
157- < div className = "flex flex-wrap gap-2 mt-2" >
158- { selectedTags . map ( ( tag ) => (
159- < Badge
160- key = { tag }
161- variant = "secondary"
162- className = "flex items-center gap-1"
163- >
164- < span > { tag } </ span >
165- < button
166- type = "button"
167- onClick = { ( ) => handleTagRemove ( tag ) }
168- className = "ml-1 text-red-500 hover:text-red-700 text-xs"
169- disabled = { disabled }
170- >
171- ✖
172- </ button >
173- </ Badge >
174- ) ) }
175- </ div >
176- ) }
177197 </ div >
178198 ) ;
179199} ;
0 commit comments