@ -1,13 +1,17 @@
import { css } from '@emotion/css' ;
import { useVirtualizer } from '@tanstack/react-virtual' ;
import { useCombobox } from 'downshift' ;
import React , { useMemo , useState } from 'react' ;
import React , { useMemo , useRef , use State } from 'react' ;
import { useStyles2 } from '../../themes' ;
import { Icon } from '../Icon/Icon' ;
import { Input , Props as InputProps } from '../Input/Input' ;
type Value = string | number ;
type Option = {
export type Value = string | number ;
export type Option = {
label : string ;
value : Value ;
description? : string ;
} ;
interface ComboboxProps
@ -33,32 +37,86 @@ function itemFilter(inputValue: string) {
} ;
}
function estimateSize() {
return 60 ;
}
export const Combobox = ( { options , onChange , value , . . . restProps } : ComboboxProps ) = > {
const [ items , setItems ] = useState ( options ) ;
const selectedItem = useMemo ( ( ) = > options . find ( ( option ) = > option . value === value ) || null , [ options , value ] ) ;
const listRef = useRef ( null ) ;
const styles = useStyles2 ( getStyles ) ;
const rowVirtualizer = useVirtualizer ( {
count : items.length ,
getScrollElement : ( ) = > listRef . current ,
estimateSize ,
overscan : 2 ,
} ) ;
const { getInputProps , getMenuProps , getItemProps , isOpen } = useCombobox ( {
items ,
itemToString ,
selectedItem ,
scrollIntoView : ( ) = > { } ,
onInputValueChange : ( { inputValue } ) = > {
setItems ( options . filter ( itemFilter ( inputValue ) ) ) ;
} ,
onSelectedItemChange : ( { selectedItem } ) = > onChange ( selectedItem ) ,
onHighlightedIndexChange : ( { highlightedIndex , type } ) = > {
if ( type !== useCombobox . stateChangeTypes . MenuMouseLeave ) {
rowVirtualizer . scrollToIndex ( highlightedIndex ) ;
}
} ,
} ) ;
return (
< div >
< Input suffix = { < Icon name = { isOpen ? 'search' : 'angle-down' } / > } { ...restProps } { ...getInputProps ( ) } / >
< ul { ...getMenuProps ( ) } >
{ isOpen &&
items . map ( ( item , index ) = > {
return (
< li key = { item . value } { ...getItemProps ( { item , index } ) } >
{ item . label }
< / li >
) ;
} ) }
< / ul >
< div className = { styles . dropdown } { ...getMenuProps ( { ref : listRef } ) } >
{ isOpen && (
< ul style = { { height : rowVirtualizer.getTotalSize ( ) } } >
{ rowVirtualizer . getVirtualItems ( ) . map ( ( virtualRow ) = > {
return (
< li
key = { items [ virtualRow . index ] . value }
{ . . . getItemProps ( { item : items [ virtualRow . index ] , index : virtualRow.index } ) }
data - index = { virtualRow . index }
ref = { rowVirtualizer . measureElement }
className = { styles . menuItem }
style = { {
transform : ` translateY( ${ virtualRow . start } px) ` ,
} }
>
< span > { items [ virtualRow . index ] . label } < / span >
{ items [ virtualRow . index ] . description && < span > { items [ virtualRow . index ] . description } < / span > }
< / li >
) ;
} ) }
< / ul >
) }
< / div >
< / div >
) ;
} ;
const getStyles = ( ) = > ( {
dropdown : css ( {
position : 'absolute' ,
height : 400 ,
width : 600 ,
overflowY : 'scroll' ,
contain : 'strict' ,
} ) ,
menuItem : css ( {
position : 'absolute' ,
top : 0 ,
left : 0 ,
width : '100%' ,
display : 'flex' ,
flexDirection : 'column' ,
'&:first-child' : {
fontWeight : 'bold' ,
} ,
} ) ,
} ) ;