@@ -10,7 +10,7 @@ import {
1010} from '@headlessui/react' ;
1111import { ChevronDown , X } from 'lucide-react' ;
1212import { ChangeEvent , ComponentProps , ReactNode , useState } from 'react' ;
13- import { isNonNullish , isNullish } from 'remeda' ;
13+ import { isEmpty , isNonNullish , isNullish } from 'remeda' ;
1414
1515import { cn } from '@/lib/tailwind/utils' ;
1616import { getUiState } from '@/lib/ui-state' ;
@@ -34,7 +34,9 @@ type SelectProps<TValue extends TValueBase> = ComboboxProps<TValue, false> &
3434 renderEmpty ?: ( search : string ) => ReactNode ;
3535 /** Allow the user to provide a custom value */
3636 allowCustomValue ?: boolean ;
37+ /** Allow you to provide custom ComboboxOption */
3738 renderOption ?: ( option : OptionBase ) => ReactNode ;
39+ mode ?: 'default' | 'virtual' ;
3840 } ;
3941
4042export const Select = < TValue extends TValueBase > ( {
@@ -49,19 +51,13 @@ export const Select = <TValue extends TValueBase>({
4951 value,
5052 defaultValue,
5153 allowCustomValue = false ,
54+ mode = 'default' ,
5255 ...props
5356} : SelectProps < TValue > ) => {
54- const [ items , setItems ] = useState ( options ) ;
5557 const [ search , setSearch ] = useState ( '' ) ;
5658
5759 const handleInputChange = ( event : ChangeEvent < HTMLInputElement > ) => {
5860 setSearch ( event . target . value ) ;
59-
60- setItems (
61- options . filter ( ( item ) =>
62- item . label . toLowerCase ( ) . includes ( event . target . value . toLowerCase ( ) )
63- )
64- ) ;
6561 } ;
6662
6763 const handleOnValueChange : SelectProps < TValue > [ 'onChange' ] = ( value ) => {
@@ -74,9 +70,12 @@ export const Select = <TValue extends TValueBase>({
7470 */
7571 const handleOnClose = ( ) => {
7672 setSearch ( '' ) ;
77- setItems ( options ) ;
7873 } ;
7974
75+ const items = options . filter ( ( item ) =>
76+ item . label . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
77+ ) ;
78+
8079 const ui = getUiState ( ( set ) => {
8180 if ( items . length === 0 && allowCustomValue && search . length > 0 ) {
8281 return set ( 'create-search' , { search } ) ;
@@ -90,6 +89,10 @@ export const Select = <TValue extends TValueBase>({
9089 return set ( 'empty-override' , { renderEmpty } ) ;
9190 }
9291
92+ if ( mode === 'virtual' && ! isEmpty ( items ) ) {
93+ return set ( 'virtual' ) ;
94+ }
95+
9396 if ( items . length !== 0 && isNonNullish ( renderOption ) ) {
9497 return set ( 'render-option' , { renderOption } ) ;
9598 }
@@ -107,6 +110,11 @@ export const Select = <TValue extends TValueBase>({
107110 value = { value ?? null }
108111 onChange = { ( v ) => handleOnValueChange ( v ) }
109112 onClose = { handleOnClose }
113+ virtual = {
114+ mode === 'virtual' && isNonNullish ( items ) && ! isEmpty ( items )
115+ ? { options : items , disabled : ( o ) => o ?. disabled }
116+ : undefined
117+ }
110118 { ...props }
111119 >
112120 < div className = "relative" >
@@ -157,6 +165,9 @@ export const Select = <TValue extends TValueBase>({
157165 Create < span className = "font-bold" > { search } </ span >
158166 </ ComboboxOption >
159167 ) )
168+ . match ( 'virtual' , ( ) => ( { option } : { option : OptionBase } ) => (
169+ < ComboboxOption value = { option } > { option . label } </ ComboboxOption >
170+ ) )
160171 . match ( 'render-option' , ( { renderOption } ) =>
161172 items . map ( ( item ) => renderOption ( item ) )
162173 )
@@ -188,7 +199,7 @@ export function ComboboxOption({
188199 'flex cursor-pointer gap-1 rounded-sm px-3 py-1.5' ,
189200 'data-[focus]:bg-neutral-50 dark:data-[focus]:bg-neutral-800' ,
190201 'data-[selected]:bg-neutral-100 data-[selected]:font-medium dark:data-[selected]:bg-neutral-700' ,
191- 'data-[ disabled] :cursor-not-allowed data-[ disabled] :opacity-50' ,
202+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50' ,
192203 'text-sm hover:bg-neutral-50 dark:hover:bg-neutral-800' ,
193204 props . className
194205 ) }
0 commit comments