1- import React , { useMemo } from 'react' ;
1+ import React , { useMemo , useState } from 'react' ;
22import ReactSelect from 'react-select' ;
33import cn from 'classnames' ;
44
55import * as css from './Select.module.css' ;
66
7- const toOption = ( value ) => ( { value, label : value } ) ;
8-
97export const Select = ( {
108 title,
119 options,
@@ -17,8 +15,12 @@ export const Select = ({
1715 variant,
1816 instanceId
1917} ) => {
18+ const [ input , setInput ] = useState ( '' ) ;
2019 const opts = useMemo ( ( ) => options . map ( toOption ) , [ options ] ) ;
21- const handleOnChange = ( o , action ) => onChange ( o ? o . value : o ) ;
20+ const filteredOpts = useMemo (
21+ ( ) => ( input === '' ? opts : filterAndSortRank ( opts , input ) ) ,
22+ [ opts , input ]
23+ ) ;
2224
2325 return (
2426 < section className = { cn ( css . root , className , { [ css [ variant ] ] : variant } ) } >
@@ -36,10 +38,12 @@ export const Select = ({
3638 placeholder = { placeholder }
3739 className = { css . select }
3840 classNamePrefix = "rs"
39- options = { opts }
40- defaultValue = { selected ? toOption ( selected ) : selected }
41- onChange = { handleOnChange }
41+ options = { filteredOpts }
42+ defaultValue = { selected ? toOption ( selected ) : '' }
43+ onChange = { ( o ) => onChange ( o ? o . value : null ) }
4244 instanceId = { instanceId }
45+ onInputChange = { setInput }
46+ filterOption = { ( ) => true }
4347 />
4448
4549 < div className = { css . itemSpacer } > </ div >
@@ -48,4 +52,49 @@ export const Select = ({
4852 </ section >
4953 ) ;
5054} ;
55+
5156export default Select ;
57+
58+ const toOption = ( value ) => ( { value, label : value } ) ;
59+
60+ // trim, lowercase and strip accents
61+ const normalize = ( value ) =>
62+ value
63+ . trim ( )
64+ . toLowerCase ( )
65+ . normalize ( 'NFD' )
66+ . replace ( / [ \u0300 - \u036f ] / g, '' ) ;
67+
68+ const rank = ( value , input ) => {
69+ // exact match: highest priority
70+ if ( value === input ) return 0 ;
71+
72+ // complete word match: higher priority based on word position
73+ const words = value . split ( ' ' ) ;
74+ for ( let i = 0 ; i < words . length ; i ++ ) {
75+ if ( words [ i ] === input ) return i + 1 ;
76+ }
77+
78+ // partial match: lower priority based on character position
79+ const index = value . indexOf ( input ) ;
80+ return index === - 1 ? Number . MAX_SAFE_INTEGER : 1000 + index ;
81+ } ;
82+
83+ const filterAndSortRank = ( options , input ) => {
84+ // It doesn't seem possible to only sort the filtered options in react-select, but we can re-implement the filtering to do so.
85+ // https://github.com/JedWatson/react-select/discussions/4426
86+
87+ const normalizedInput = normalize ( input ) ;
88+
89+ return options
90+ . filter ( ( o ) => normalize ( o . value ) . includes ( normalizedInput ) )
91+ . sort ( ( optA , optB ) => {
92+ const rankDelta =
93+ rank ( normalize ( optA . value ) , normalizedInput ) -
94+ rank ( normalize ( optB . value ) , normalizedInput ) ;
95+
96+ if ( rankDelta !== 0 ) return rankDelta ;
97+
98+ return optA . value . localeCompare ( optB . value ) ;
99+ } ) ;
100+ } ;
0 commit comments