1- import React , { useCallback } from "react" ;
1+ import React , { useCallback , useState } from "react" ;
22import PT from "prop-types" ;
33import cn from "classnames" ;
4- import AsyncSelect from "react-select/async" ;
4+ import throttle from "lodash/throttle" ;
5+ import Select , { components } from "react-select" ;
56import { getMemberSuggestions } from "services/teams" ;
7+ import { useUpdateEffect } from "utils/hooks" ;
68import styles from "./styles.module.scss" ;
79
10+ const loadingMessage = ( ) => "Loading..." ;
11+
12+ const noOptionsMessage = ( ) => "No suggestions" ;
13+
14+ function MenuList ( props ) {
15+ let focusedOption = props . focusedOption ;
16+ focusedOption = props . selectProps . isMenuFocused
17+ ? focusedOption
18+ : props . getValue ( ) [ 0 ] ;
19+
20+ return < components . MenuList { ...props } focusedOption = { focusedOption } /> ;
21+ }
22+ MenuList . propTypes = {
23+ focusedOption : PT . object ,
24+ getValue : PT . func ,
25+ selectProps : PT . shape ( {
26+ isMenuFocused : PT . oneOfType ( [ PT . bool , PT . number ] ) ,
27+ } ) ,
28+ } ;
29+
30+ function Option ( props ) {
31+ return (
32+ < components . Option
33+ { ...props }
34+ isFocused = { props . selectProps . isMenuFocused && props . isFocused }
35+ isSelected = { ! props . selectProps . isMenuFocused && props . isSelected }
36+ />
37+ ) ;
38+ }
39+ Option . propTypes = {
40+ isFocused : PT . bool ,
41+ isSelected : PT . bool ,
42+ selectProps : PT . shape ( {
43+ isMenuFocused : PT . oneOfType ( [ PT . bool , PT . number ] ) ,
44+ } ) ,
45+ } ;
46+
847const selectComponents = {
948 DropdownIndicator : ( ) => null ,
49+ ClearIndicator : ( ) => null ,
1050 IndicatorSeparator : ( ) => null ,
51+ MenuList,
52+ Option,
1153} ;
1254
13- const loadingMessage = ( ) => "Loading..." ;
14-
15- const noOptionsMessage = ( ) => "No suggestions" ;
16-
1755/**
1856 * Displays search input field.
1957 *
@@ -23,10 +61,9 @@ const noOptionsMessage = () => "No suggestions";
2361 * @param {string } props.placeholder placeholder text
2462 * @param {string } props.name name for input element
2563 * @param {'medium'|'small' } [props.size] field size
26- * @param {function } props.onChange function called when input value changes
64+ * @param {function } props.onChange function called when value changes
2765 * @param {function } [props.onInputChange] function called when input value changes
28- * @param {function } [props.onBlur] function called when input is blurred
29- * @param {function } [props.onMenuClose] function called when option list is closed
66+ * @param {function } [props.onBlur] function called on input blur
3067 * @param {string } props.value input value
3168 * @returns {JSX.Element }
3269 */
@@ -38,70 +75,139 @@ const SearchHandleField = ({
3875 onChange,
3976 onInputChange,
4077 onBlur,
41- onMenuClose,
4278 placeholder,
4379 value,
4480} ) => {
45- const onValueChange = useCallback (
46- ( option , { action } ) => {
47- if ( action === "clear" ) {
48- onChange ( "" ) ;
49- } else {
81+ const [ inputValue , setInputValue ] = useState ( value ) ;
82+ const [ isLoading , setIsLoading ] = useState ( false ) ;
83+ const [ isMenuOpen , setIsMenuOpen ] = useState ( false ) ;
84+ const [ isMenuFocused , setIsMenuFocused ] = useState ( false ) ;
85+ const [ options , setOptions ] = useState ( [ ] ) ;
86+
87+ const loadOptions = useCallback (
88+ throttle (
89+ async ( value ) => {
90+ setIsLoading ( true ) ;
91+ const options = await loadSuggestions ( value ) ;
92+ setOptions ( options ) ;
93+ setIsLoading ( false ) ;
94+ setIsMenuOpen ( true ) ;
95+ setIsMenuFocused ( options . length && options [ 0 ] . value === value ) ;
96+ } ,
97+ 300 ,
98+ { leading : false }
99+ ) ,
100+ [ ]
101+ ) ;
102+
103+ const onValueChange = ( option , { action } ) => {
104+ if ( action === "input-change" || action === "select-option" ) {
105+ setIsMenuFocused ( false ) ;
106+ setIsMenuOpen ( false ) ;
107+ if ( ! isMenuFocused ) {
108+ onChange ( inputValue ) ;
109+ } else if ( option ) {
50110 onChange ( option . value ) ;
51111 }
52- } ,
53- [ onChange ]
54- ) ;
112+ } else if ( action === "clear" ) {
113+ setIsMenuFocused ( false ) ;
114+ setIsMenuOpen ( false ) ;
115+ onChange ( "" ) ;
116+ }
117+ } ;
55118
56119 const onInputValueChange = useCallback (
57120 ( value , { action } ) => {
58121 if ( action === "input-change" ) {
59- onInputChange ( value ) ;
122+ setIsMenuFocused ( false ) ;
123+ setInputValue ( value ) ;
124+ onInputChange && onInputChange ( value ) ;
125+ loadOptions ( value ) ;
60126 }
61127 } ,
62- [ onInputChange ]
128+ [ onInputChange , loadOptions ]
63129 ) ;
64130
131+ const onKeyDown = ( event ) => {
132+ const key = event . key ;
133+ if ( key === "Enter" || key === "Escape" ) {
134+ setIsMenuFocused ( false ) ;
135+ setIsMenuOpen ( false ) ;
136+ if ( ! inputValue ) {
137+ onChange ( inputValue ) ;
138+ }
139+ } else if ( key === "ArrowDown" ) {
140+ if ( ! isMenuFocused ) {
141+ event . preventDefault ( ) ;
142+ event . stopPropagation ( ) ;
143+ setIsMenuFocused ( true ) ;
144+ }
145+ } else if ( key === "Backspace" ) {
146+ if ( ! inputValue ) {
147+ event . preventDefault ( ) ;
148+ event . stopPropagation ( ) ;
149+ }
150+ }
151+ } ;
152+
153+ const onSelectBlur = useCallback ( ( ) => {
154+ setIsMenuFocused ( false ) ;
155+ setIsMenuOpen ( false ) ;
156+ onBlur && onBlur ( ) ;
157+ } , [ onBlur ] ) ;
158+
159+ useUpdateEffect ( ( ) => {
160+ setInputValue ( value ) ;
161+ } , [ value ] ) ;
162+
65163 return (
66164 < div className = { cn ( styles . container , styles [ size ] , className ) } >
67165 < span className = { styles . icon } />
68- < AsyncSelect
166+ < Select
69167 className = { styles . select }
70168 classNamePrefix = "custom"
71169 components = { selectComponents }
72170 id = { id }
73171 name = { name }
74172 isClearable = { true }
75173 isSearchable = { true }
76- // menuIsOpen={true} // for debugging
174+ isLoading = { isLoading }
175+ isMenuFocused = { isMenuFocused }
176+ menuIsOpen = { isMenuOpen }
77177 value = { null }
78- inputValue = { value }
178+ inputValue = { inputValue }
179+ options = { options }
79180 onChange = { onValueChange }
80181 onInputChange = { onInputValueChange }
81- onBlur = { onBlur }
82- onMenuClose = { onMenuClose }
83- openMenuOnClick = { false }
182+ onKeyDown = { onKeyDown }
183+ onBlur = { onSelectBlur }
84184 placeholder = { placeholder }
85185 noOptionsMessage = { noOptionsMessage }
86186 loadingMessage = { loadingMessage }
87- loadOptions = { loadSuggestions }
88- cacheOptions
89187 />
90188 </ div >
91189 ) ;
92190} ;
93191
94- const loadSuggestions = async ( inputVal ) => {
192+ const loadSuggestions = async ( inputValue ) => {
95193 let options = [ ] ;
96- if ( inputVal . length < 3 ) {
194+ if ( inputValue . length < 3 ) {
97195 return options ;
98196 }
99197 try {
100- const res = await getMemberSuggestions ( inputVal ) ;
198+ const res = await getMemberSuggestions ( inputValue ) ;
101199 const users = res . data . result . content ;
200+ let match = null ;
102201 for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
103202 let value = users [ i ] . handle ;
104- options . push ( { value, label : value } ) ;
203+ if ( value === inputValue ) {
204+ match = { value, label : value } ;
205+ } else {
206+ options . push ( { value, label : value } ) ;
207+ }
208+ }
209+ if ( match ) {
210+ options . unshift ( match ) ;
105211 }
106212 } catch ( error ) {
107213 console . error ( error ) ;
@@ -118,7 +224,6 @@ SearchHandleField.propTypes = {
118224 onChange : PT . func . isRequired ,
119225 onInputChange : PT . func ,
120226 onBlur : PT . func ,
121- onMenuClose : PT . func ,
122227 placeholder : PT . string ,
123228 value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
124229} ;
0 commit comments