import { useState , useRef , useMemo , FormEvent , KeyboardEvent , MouseEvent , ReactElement , ComponentPropsWithoutRef , } from 'react' ; import { c } from 'ttag' ; import { normalize } from '@proton/shared/lib/helpers/string' ; import { Dropdown } from '../dropdown' ; import { Props as OptionProps } from '../option/Option' ; import { classnames } from '../../helpers' ; import { SearchInput } from '../input' ; import SelectButton from './SelectButton' ; import SelectOptions from './SelectOptions' ; import useSelect , { SelectProvider } from './useSelect' ; import { SelectChangeEvent } from './select' ; const includesString = ( str1 , str2 ) => normalize ( str1 , true ) . indexOf ( normalize ( str2 , true ) ) > - 1 ; const arrayIncludesString = ( arrayToSearch , keyword ) => arrayToSearch . some ( ( str ) => includesString ( str , keyword ) ) ; const defaultFilterFunction = < V > ( option , keyword ) => ( option . title && includesString ( option . title , keyword ) ) || ( option . searchStrings && arrayIncludesString ( option . searchStrings , keyword ) ) ; export interface Props < V > extends Omit < ComponentPropsWithoutRef < 'button' > , 'value' | 'onClick' | 'onChange' | 'onKeyDown' | 'aria-label' > { value ? : V ; /**
     * Optionally allows controlling the Select's open state
     */ isOpen ? : boolean ; /**
     * Children Options of the Select, have to be of type Option
     * (or something that implements the same interface)
     */ children : ReactElement < OptionProps < V > > [ ] ; onChange ? : ( e : SelectChangeEvent < V > ) => void ; onClose ? : ( ) => void ; onOpen ? : ( ) => void ; loading ? : boolean ; search ? : boolean | ( ( option : OptionProps < V > ) => void ) ; searchPlaceholder ? : string ; noSearchResults ? : string ; } const SearchableSelect = < V extends any > ( { children , value , placeholder , isOpen : controlledOpen , onClose , onOpen , onChange , loading , search , searchPlaceholder , noSearchResults = c ( 'Select search results' ) . t `No results found` , ... rest } ) => { const [ searchValue , setSearchValue ] = useState ( '' ) ; const anchorRef = useRef < HTMLButtonElement | null > ( null ) ; const searchContainerRef = useRef < HTMLDivElement > ( null ) ; const searchInputRef = useRef < HTMLInputElement > ( null ) ; const optionValues = children . map ( ( option ) => option . props . value ) ; const select = useSelect < V > ( { value , options : optionValues , onChange , onClose , onOpen , } ) ; const { isOpen , selectedIndex , open , close : handleClose , setFocusedIndex , handleChange , focusNextIndex , focusPreviousIndex , focusedIndex , } = select ; const close = ( event ? ) => { if ( event ?. target instanceof Node && searchContainerRef ?. current ?. contains ( event . target ) ) { return ; } handleClose ( ) ; } ; const focusSearchInput = ( ) => { searchInputRef ?. current ?. focus ( ) ; } ; const handleAnchorClick = ( ) => { if ( isOpen ) { close ( ) ; } else { open ( ) ; setFocusedIndex ( selectedIndex || 0 ) ; } } ; const handleClosed = ( ) => { setSearchValue ( '' ) ; } ; const handleDropdownContentKeyDown = ( e ) => { switch ( e . key ) { case 'Escape' : { close ( ) ; anchorRef . current ?. focus ( ) ; break ; } default : } } ; const onSearchChange = ( event ) => { setSearchValue ( event . currentTarget . value ) ; if ( ! event . currentTarget . value ) { focusSearchInput ( ) ; } } ; const selectedChild = selectedIndex || selectedIndex === 0 ? children [ selectedIndex ] : null ; const displayedValue = selectedChild ?. props ?. children || selectedChild ?. props ?. title || placeholder ; const ariaLabel = selectedChild ?. props ?. title ; const filteredOptions = useMemo ( ( ) => { if ( ! searchValue ) { return children ; } const filterFunction = typeof search === 'function' ? search : defaultFilterFunction ; const filteredOptions = children . filter ( ( child ) => filterFunction ( child . props , searchValue ) ) ; return filteredOptions ; } , [ children , search , searchValue ] ) ; const selectedIndexInFilteredOptions = typeof selectedIndex === 'number' ? filteredOptions . findIndex ( ( option ) => children [ selectedIndex ] === option ) : null ; const pressedDown = useRef ( false ) ; return ( < SelectProvider { ... select } > < SelectButton isOpen = { isOpen } onClick = { handleAnchorClick } aria - label = { ariaLabel } ref = { anchorRef } { ... rest } > { displayedValue } < /SelectButton> < Dropdown isOpen = { isOpen } onClosed = { handleClosed } anchorRef = { anchorRef } onClose = { close } offset = { 4 } noCaret noMaxWidth sameAnchorWidth disableDefaultArrowNavigation = { ! searchValue } className = { classnames ( [ searchContainerRef ?. current && 'dropdown--is-searchable' ] ) } > < div onKeyDown = { handleDropdownContentKeyDown } > < div className = "dropdown-search" ref = { searchContainerRef } > < SearchInput onKeyDown = { ( e ) => { if ( searchValue ) { return ; } if ( e . key === 'ArrowDown' ) { focusNextIndex ( ) ; } if ( e . key === 'ArrowUp' ) { focusPreviousIndex ( ) ; } if ( e . key === 'Tab' ) { handleClose ( ) ; } if ( e . key === 'Enter' ) { pressedDown . current = true ; } } } onKeyUp = { ( e ) => { if ( searchValue ) { return ; } if ( e . key === 'Enter' && pressedDown . current ) { if ( focusedIndex ) { handleChange ( { value : optionValues [ focusedIndex ] , selectedIndex : focusedIndex , } ) ; handleClose ( ) ; } pressedDown . current = false ; } } } autoFocus ref = { searchInputRef } value = { searchValue } onInput = { onSearchChange } placeholder = { searchPlaceholder } / > < /div> { filteredOptions . length === 0 ? ( < div className = "dropdown-search-no-result text-center" > { noSearchResults } < /div>) : (<SelectOptions disableFocusOnActive selected={selectedIndexInFilteredOptions} onChange={handleChange}> { filteredOptions } < /SelectOptions>)} < /div> < /Dropdown> < /SelectProvider>); } ; export default SearchableSelect ;