import style from "./Select.module.scss" ; import effects from "../../styles/Effects.module.scss" ; import cs from "classnames" ; import { InputHTMLAttributes , useMemo , useState , useRef , useEffect } from "react" ; import { Cover } from "../Utils/Cover" ; import { useClientAsyncEffect } from "../../utils/hookts" ; import { InputText } from "./InputText" ; import type FuzzySearchType from "fuzzy-search" ; export interface IOptions { label : string ; value : any ; disabled ? : boolean ; } interface Props extends InputHTMLAttributes < HTMLSelectElement > { options : IOptions [ ] ; value : any ; onChange : ( value : any ) => void ; className ? : string ; search ? : boolean ; searchKeys ? : string [ ] ; searchDictionnary ? : any [ ] ; searchValue ? : string ; } export function Select ( { options , value , onChange , placeholder , className , search = false , searchKeys , searchDictionnary , searchValue , ... props } ) { const selectRef = useRef < HTMLDivElement > ( null ) ; const [ searchString , setSearchString ] = useState < string > ( "" ) ; const searcherRef = useRef < FuzzySearchType < any > | null > ( null ) ; const [ searchResults , setSearchResults ] = useState < IOptions [ ] | null > ( null ) ; const searchRef = useRef < HTMLInputElement > ( null ) ; const [ opened , setOpened ] = useState < boolean > ( false ) ; const [ direction , setDirection ] = useState < "top" | "bottom" > ( "bottom" ) ; const selectedOption = useMemo < IOptions > ( ( ) => { return options . find ( opt => opt . value === value ) || options [ 0 ] ; } , [ value ] ) ; const toggleOpened = ( ) => { if ( ! opened && selectRef . current ) { const bounds = selectRef . current . getBoundingClientRect ( ) ; setDirection ( window . innerHeight - bounds . bottom < 250 ? "top" : "bottom" ) ; } setOpened ( ! opened ) ; } ; const updateValue = ( val ) => { onChange ( val ) ; setOpened ( false ) ; } ; useClientAsyncEffect ( async ( isMounted ) => { if ( search ) { const FuzzySearch = ( await import ( "fuzzy-search" ) ) . default ; searcherRef . current = new FuzzySearch ( searchDictionnary || [ ] , searchKeys || [ ] ) ; } } , [ search ] ) ; useEffect ( ( ) => { if ( opened && searchRef . current ) { searchRef . current . focus ( ) ; } } , [ opened ] ) ; useEffect ( ( ) => { if ( search && searcherRef . current ) { if ( searchString . length > 0 ) { const results = searcherRef . current . search ( searchString ) ; const found = [ ] ; for ( const result of results ) { const f = options . find ( opt => opt . value === result [ searchValue || "value" ] ) ; if ( f ) found . push ( f ) ; } setSearchResults ( found ) ; } else { setSearchResults ( null ) ; } } } , [ searchString ] ) ; const displayOptions = searchResults || options ; return ( < > < div className = { cs ( style . root , style [ `opening_${ direction }` ] ) } ref = { selectRef } > < button className = { cs ( style . select , className , { [ style . opened ] : opened } ) } onClick = { toggleOpened } type = "button" > { placeholder && value === "" ? ( < > < div className = { cs ( style . placeholder ) } > { placeholder } < /div> < div aria - hidden = "true" className = { cs ( style . sizer ) } > { placeholder } < /div> < />) : (<div>{selectedOption.label}</div > ) } { options . map ( ( opt , idx ) => ( < div key = { idx } aria - hidden = "true" className = { cs ( style . sizer ) } > { opt . label } < /div>))} < /button> { opened && ( < div className = { cs ( style . options , effects [ 'drop-shadow-big' ] , { [ style . has_search ] : search } ) } > { search && ( < InputText value = { searchString } onChange = { ( evt ) => setSearchString ( evt . target . value ) } placeholder = "Search..." ref = { searchRef } className = { cs ( style . search , effects [ 'drop-shadow-big' ] ) } / > ) } < div className = { cs ( style . options_wrapper ) } > < div > { displayOptions . map ( ( option , idx ) => ( < button key = { idx } className = { cs ( style . option ) } onClick = { ( ) => updateValue ( option . value ) } disabled = { option . disabled } type = "button" > { option . label } < /button>))} < /div> < /div> < /div>)} < /div> { opened && ( < Cover onClick = { ( ) => setOpened ( false ) } opacity = { 0 } index = { 10 } / > ) } < />); }