1
+ import { Search } from 'lucide-react' ;
1
2
import * as Ariakit from '@ariakit/react' ;
2
3
import { matchSorter } from 'match-sorter' ;
3
- import { AutoSizer , List } from 'react-virtualized' ;
4
- import { startTransition , useMemo , useState , useEffect , useRef , memo } from 'react' ;
5
- import { cn } from '~/utils' ;
4
+ import { useMemo , useState , useRef , memo , useEffect } from 'react' ;
5
+ import { SelectRenderer } from '@ariakit/react-core/select/select-renderer' ;
6
6
import type { OptionWithIcon } from '~/common' ;
7
- import { Search } from 'lucide-react ' ;
7
+ import { cn } from '~/utils ' ;
8
8
9
9
interface ControlComboboxProps {
10
10
selectedValue : string ;
@@ -35,11 +35,33 @@ function ControlCombobox({
35
35
const buttonRef = useRef < HTMLButtonElement > ( null ) ;
36
36
const [ buttonWidth , setButtonWidth ] = useState < number | null > ( null ) ;
37
37
38
+ const getItem = ( option : OptionWithIcon ) => ( {
39
+ id : `item-${ option . value } ` ,
40
+ value : option . value as string | undefined ,
41
+ label : option . label ,
42
+ icon : option . icon ,
43
+ } ) ;
44
+
45
+ const combobox = Ariakit . useComboboxStore ( {
46
+ defaultItems : items . map ( getItem ) ,
47
+ resetValueOnHide : true ,
48
+ value : searchValue ,
49
+ setValue : setSearchValue ,
50
+ } ) ;
51
+
52
+ const select = Ariakit . useSelectStore ( {
53
+ combobox,
54
+ defaultItems : items . map ( getItem ) ,
55
+ value : selectedValue ,
56
+ setValue,
57
+ } ) ;
58
+
38
59
const matches = useMemo ( ( ) => {
39
- return matchSorter ( items , searchValue , {
60
+ const filteredItems = matchSorter ( items , searchValue , {
40
61
keys : [ 'value' , 'label' ] ,
41
62
baseSort : ( a , b ) => ( a . index < b . index ? - 1 : 1 ) ,
42
63
} ) ;
64
+ return filteredItems . map ( getItem ) ;
43
65
} , [ searchValue , items ] ) ;
44
66
45
67
useEffect ( ( ) => {
@@ -48,104 +70,74 @@ function ControlCombobox({
48
70
}
49
71
} , [ isCollapsed ] ) ;
50
72
51
- const rowRenderer = ( {
52
- index,
53
- key,
54
- style,
55
- } : {
56
- index : number ;
57
- key : string ;
58
- style : React . CSSProperties ;
59
- } ) => {
60
- const item = matches [ index ] ;
61
- return (
62
- < Ariakit . SelectItem
63
- key = { key }
64
- value = { `${ item . value ?? '' } ` }
65
- aria-label = { `${ item . label ?? item . value ?? '' } ` }
73
+ return (
74
+ < div className = "flex w-full items-center justify-center px-1" >
75
+ < Ariakit . SelectLabel store = { select } className = "sr-only" >
76
+ { ariaLabel }
77
+ </ Ariakit . SelectLabel >
78
+ < Ariakit . Select
79
+ ref = { buttonRef }
80
+ store = { select }
66
81
className = { cn (
67
- 'flex cursor-pointer items -center px-3 text-sm ' ,
82
+ 'flex items-center justify -center gap-2 rounded-full bg-surface-secondary ' ,
68
83
'text-text-primary hover:bg-surface-tertiary' ,
69
- 'data-[active-item]:bg-surface-tertiary' ,
84
+ 'border border-border-light' ,
85
+ isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm' ,
70
86
) }
71
- render = { < Ariakit . ComboboxItem /> }
72
- style = { style }
73
87
>
74
- { item . icon != null && (
75
- < div className = "assistant-item mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full" >
76
- { item . icon }
88
+ { SelectIcon != null && (
89
+ < div className = "assistant-item flex h-5 w-5 items-center justify-center overflow-hidden rounded-full" >
90
+ { SelectIcon }
77
91
</ div >
78
92
) }
79
- < span className = "flex-grow truncate text-left" > { item . label } </ span >
80
- </ Ariakit . SelectItem >
81
- ) ;
82
- } ;
83
-
84
- return (
85
- < div className = "flex w-full items-center justify-center px-1" >
86
- < Ariakit . ComboboxProvider
87
- resetValueOnHide
88
- setValue = { ( value ) => {
89
- startTransition ( ( ) => {
90
- setSearchValue ( value ) ;
91
- } ) ;
92
- } }
93
+ { ! isCollapsed && (
94
+ < span className = "flex-grow truncate text-left" > { displayValue ?? selectPlaceholder } </ span >
95
+ ) }
96
+ </ Ariakit . Select >
97
+ < Ariakit . SelectPopover
98
+ store = { select }
99
+ gutter = { 4 }
100
+ portal
101
+ className = "z-50 overflow-hidden rounded-md border border-border-light bg-surface-secondary shadow-lg"
102
+ style = { { width : isCollapsed ? '300px' : ( buttonWidth ?? '300px' ) } }
93
103
>
94
- < Ariakit . SelectProvider value = { selectedValue } setValue = { setValue } >
95
- < Ariakit . SelectLabel className = "sr-only" > { ariaLabel } </ Ariakit . SelectLabel >
96
- < Ariakit . Select
97
- ref = { buttonRef }
98
- className = { cn (
99
- 'flex items-center justify-center gap-2 rounded-full bg-surface-secondary' ,
100
- 'text-text-primary hover:bg-surface-tertiary' ,
101
- 'border border-border-light' ,
102
- isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm' ,
103
- ) }
104
- >
105
- { SelectIcon != null && (
106
- < div className = "assistant-item flex h-5 w-5 items-center justify-center overflow-hidden rounded-full" >
107
- { SelectIcon }
108
- </ div >
109
- ) }
110
- { ! isCollapsed && (
111
- < span className = "flex-grow truncate text-left" >
112
- { displayValue ?? selectPlaceholder }
113
- </ span >
114
- ) }
115
- </ Ariakit . Select >
116
- < Ariakit . SelectPopover
117
- gutter = { 4 }
118
- portal
119
- className = "z-50 overflow-hidden rounded-md border border-border-light bg-surface-secondary shadow-lg"
120
- style = { { width : isCollapsed ? '300px' : buttonWidth ?? '300px' } }
121
- >
122
- < div className = "p-2" >
123
- < div className = "relative" >
124
- < Search className = "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
125
- < Ariakit . Combobox
126
- autoSelect
127
- placeholder = { searchPlaceholder }
128
- className = "w-full rounded-md border border-border-light bg-surface-tertiary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
129
- />
130
- </ div >
131
- </ div >
132
- < div className = "max-h-[50vh]" >
133
- < AutoSizer disableHeight >
134
- { ( { width } ) => (
135
- < List
136
- width = { width }
137
- height = { Math . min ( matches . length * ROW_HEIGHT , 300 ) }
138
- rowCount = { matches . length }
139
- rowHeight = { ROW_HEIGHT }
140
- rowRenderer = { rowRenderer }
141
- overscanRowCount = { 5 }
142
- />
143
- ) }
144
- </ AutoSizer >
145
- </ div >
146
- </ Ariakit . SelectPopover >
147
- </ Ariakit . SelectProvider >
148
- </ Ariakit . ComboboxProvider >
104
+ < div className = "p-2" >
105
+ < div className = "relative" >
106
+ < Search className = "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
107
+ < Ariakit . Combobox
108
+ store = { combobox }
109
+ autoSelect
110
+ placeholder = { searchPlaceholder }
111
+ className = "w-full rounded-md border border-border-light bg-surface-tertiary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
112
+ />
113
+ </ div >
114
+ </ div >
115
+ < div className = "max-h-[300px] overflow-auto" >
116
+ < Ariakit . ComboboxList store = { combobox } >
117
+ < SelectRenderer store = { select } items = { matches } itemSize = { ROW_HEIGHT } overscan = { 5 } >
118
+ { ( { value, icon, label, ...item } ) => (
119
+ < Ariakit . ComboboxItem
120
+ key = { item . id }
121
+ { ...item }
122
+ className = { cn (
123
+ 'flex w-full cursor-pointer items-center px-3 text-sm' ,
124
+ 'text-text-primary hover:bg-surface-tertiary' ,
125
+ 'data-[active-item]:bg-surface-tertiary' ,
126
+ ) }
127
+ render = { < Ariakit . SelectItem value = { value } /> }
128
+ >
129
+ { icon != null && (
130
+ < div className = "assistant-item mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full" >
131
+ { icon }
132
+ </ div >
133
+ ) }
134
+ < span className = "flex-grow truncate text-left" > { label } </ span >
135
+ </ Ariakit . ComboboxItem >
136
+ ) }
137
+ </ SelectRenderer >
138
+ </ Ariakit . ComboboxList >
139
+ </ div >
140
+ </ Ariakit . SelectPopover >
149
141
</ div >
150
142
) ;
151
143
}
0 commit comments