1
- import React , { useCallback } from "react" ;
1
+ import React , { useCallback , useRef , useState } from "react" ;
2
2
import PT from "prop-types" ;
3
3
import cn from "classnames" ;
4
- import AsyncSelect from "react-select/async" ;
4
+ import throttle from "lodash/throttle" ;
5
+ import Select , { components } from "react-select" ;
5
6
import { getMemberSuggestions } from "services/teams" ;
7
+ import { useUpdateEffect } from "utils/hooks" ;
6
8
import styles from "./styles.module.scss" ;
7
9
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
+ const setIsMenuFocused = props . selectProps . setIsMenuFocused ;
20
+
21
+ const onMouseEnter = useCallback ( ( ) => {
22
+ setIsMenuFocused ( true ) ;
23
+ } , [ setIsMenuFocused ] ) ;
24
+
25
+ return (
26
+ < div className = { styles . menuList } onMouseEnter = { onMouseEnter } >
27
+ < components . MenuList { ...props } focusedOption = { focusedOption } />
28
+ </ div >
29
+ ) ;
30
+ }
31
+ MenuList . propTypes = {
32
+ focusedOption : PT . object ,
33
+ getValue : PT . func ,
34
+ selectProps : PT . shape ( {
35
+ isMenuFocused : PT . oneOfType ( [ PT . bool , PT . number ] ) ,
36
+ setIsMenuFocused : PT . func ,
37
+ } ) ,
38
+ } ;
39
+
40
+ function Option ( props ) {
41
+ return (
42
+ < components . Option
43
+ { ...props }
44
+ isFocused = { props . selectProps . isMenuFocused && props . isFocused }
45
+ isSelected = { ! props . selectProps . isMenuFocused && props . isSelected }
46
+ />
47
+ ) ;
48
+ }
49
+ Option . propTypes = {
50
+ isFocused : PT . bool ,
51
+ isSelected : PT . bool ,
52
+ selectProps : PT . shape ( {
53
+ isMenuFocused : PT . oneOfType ( [ PT . bool , PT . number ] ) ,
54
+ } ) ,
55
+ } ;
56
+
8
57
const selectComponents = {
9
58
DropdownIndicator : ( ) => null ,
59
+ ClearIndicator : ( ) => null ,
10
60
IndicatorSeparator : ( ) => null ,
61
+ MenuList,
62
+ Option,
11
63
} ;
12
64
13
- const loadingMessage = ( ) => "Loading..." ;
14
-
15
- const noOptionsMessage = ( ) => "No suggestions" ;
16
-
17
65
/**
18
66
* Displays search input field.
19
67
*
@@ -23,7 +71,9 @@ const noOptionsMessage = () => "No suggestions";
23
71
* @param {string } props.placeholder placeholder text
24
72
* @param {string } props.name name for input element
25
73
* @param {'medium'|'small' } [props.size] field size
26
- * @param {function } props.onChange function called when input value changes
74
+ * @param {function } props.onChange function called when value changes
75
+ * @param {function } [props.onInputChange] function called when input value changes
76
+ * @param {function } [props.onBlur] function called on input blur
27
77
* @param {string } props.value input value
28
78
* @returns {JSX.Element }
29
79
*/
@@ -33,67 +83,162 @@ const SearchHandleField = ({
33
83
name,
34
84
size = "medium" ,
35
85
onChange,
86
+ onInputChange,
87
+ onBlur,
36
88
placeholder,
37
89
value,
38
90
} ) => {
39
- const onValueChange = useCallback (
40
- ( option , { action } ) => {
41
- if ( action === "clear" ) {
42
- onChange ( "" ) ;
43
- } else {
91
+ const [ inputValue , setInputValue ] = useState ( value ) ;
92
+ const [ isLoading , setIsLoading ] = useState ( false ) ;
93
+ const [ isMenuOpen , setIsMenuOpen ] = useState ( false ) ;
94
+ const [ isMenuFocused , setIsMenuFocused ] = useState ( false ) ;
95
+ const [ options , setOptions ] = useState ( [ ] ) ;
96
+ const isChangeAppliedRef = useRef ( false ) ;
97
+
98
+ const onValueChange = ( option , { action } ) => {
99
+ if ( action === "input-change" || action === "select-option" ) {
100
+ if ( isMenuFocused && ! isLoading && option ) {
101
+ isChangeAppliedRef . current = true ;
102
+ setIsMenuFocused ( false ) ;
103
+ setIsMenuOpen ( false ) ;
104
+ setIsLoading ( false ) ;
44
105
onChange ( option . value ) ;
45
106
}
46
- } ,
47
- [ onChange ]
48
- ) ;
107
+ } else if ( action === "clear" ) {
108
+ isChangeAppliedRef . current = true ;
109
+ setIsMenuFocused ( false ) ;
110
+ setIsMenuOpen ( false ) ;
111
+ setIsLoading ( false ) ;
112
+ onChange ( "" ) ;
113
+ }
114
+ } ;
49
115
50
- const onInputChange = useCallback (
116
+ const onInputValueChange = useCallback (
51
117
( value , { action } ) => {
52
118
if ( action === "input-change" ) {
53
- onChange ( value ) ;
119
+ isChangeAppliedRef . current = false ;
120
+ setIsMenuFocused ( false ) ;
121
+ setInputValue ( value ) ;
122
+ onInputChange && onInputChange ( value ) ;
54
123
}
55
124
} ,
56
- [ onChange ]
125
+ [ onInputChange ]
126
+ ) ;
127
+
128
+ const onKeyDown = ( event ) => {
129
+ const key = event . key ;
130
+ if ( key === "Enter" || key === "Escape" ) {
131
+ if ( ! isMenuFocused || isLoading ) {
132
+ isChangeAppliedRef . current = true ;
133
+ setIsMenuFocused ( false ) ;
134
+ setIsMenuOpen ( false ) ;
135
+ setIsLoading ( false ) ;
136
+ onChange ( inputValue ) ;
137
+ }
138
+ } else if ( key === "ArrowDown" ) {
139
+ if ( ! isMenuFocused ) {
140
+ event . preventDefault ( ) ;
141
+ event . stopPropagation ( ) ;
142
+ setIsMenuFocused ( true ) ;
143
+ }
144
+ } else if ( key === "Backspace" ) {
145
+ if ( ! inputValue ) {
146
+ event . preventDefault ( ) ;
147
+ event . stopPropagation ( ) ;
148
+ }
149
+ }
150
+ } ;
151
+
152
+ const onSelectBlur = ( ) => {
153
+ setIsMenuFocused ( false ) ;
154
+ setIsMenuOpen ( false ) ;
155
+ onChange ( inputValue ) ;
156
+ onBlur && onBlur ( ) ;
157
+ } ;
158
+
159
+ const loadOptions = useCallback (
160
+ throttle (
161
+ async ( value ) => {
162
+ if ( ! isChangeAppliedRef . current ) {
163
+ setIsLoading ( true ) ;
164
+ const options = await loadSuggestions ( value ) ;
165
+ if ( ! isChangeAppliedRef . current ) {
166
+ setOptions ( options ) ;
167
+ setIsLoading ( false ) ;
168
+ setIsMenuOpen ( true ) ;
169
+ }
170
+ }
171
+ } ,
172
+ 300 ,
173
+ { leading : false }
174
+ ) ,
175
+ [ ]
57
176
) ;
58
177
178
+ useUpdateEffect ( ( ) => {
179
+ setInputValue ( value ) ;
180
+ } , [ value ] ) ;
181
+
182
+ useUpdateEffect ( ( ) => {
183
+ loadOptions ( inputValue ) ;
184
+ } , [ inputValue ] ) ;
185
+
59
186
return (
60
- < div className = { cn ( styles . container , styles [ size ] , className ) } >
187
+ < div
188
+ className = { cn (
189
+ styles . container ,
190
+ styles [ size ] ,
191
+ { [ styles . isMenuFocused ] : isMenuFocused } ,
192
+ className
193
+ ) }
194
+ >
61
195
< span className = { styles . icon } />
62
- < AsyncSelect
196
+ < Select
63
197
className = { styles . select }
64
198
classNamePrefix = "custom"
65
199
components = { selectComponents }
66
200
id = { id }
67
201
name = { name }
68
202
isClearable = { true }
69
203
isSearchable = { true }
70
- // menuIsOpen={true} // for debugging
204
+ isLoading = { isLoading }
205
+ isMenuFocused = { isMenuFocused }
206
+ setIsMenuFocused = { setIsMenuFocused }
207
+ menuIsOpen = { isMenuOpen }
71
208
value = { null }
72
- inputValue = { value }
209
+ inputValue = { inputValue }
210
+ options = { options }
73
211
onChange = { onValueChange }
74
- onInputChange = { onInputChange }
75
- openMenuOnClick = { false }
212
+ onInputChange = { onInputValueChange }
213
+ onKeyDown = { onKeyDown }
214
+ onBlur = { onSelectBlur }
76
215
placeholder = { placeholder }
77
216
noOptionsMessage = { noOptionsMessage }
78
217
loadingMessage = { loadingMessage }
79
- loadOptions = { loadSuggestions }
80
- cacheOptions
81
218
/>
82
219
</ div >
83
220
) ;
84
221
} ;
85
222
86
- const loadSuggestions = async ( inputVal ) => {
223
+ const loadSuggestions = async ( inputValue ) => {
87
224
let options = [ ] ;
88
- if ( inputVal . length < 3 ) {
225
+ if ( inputValue . length < 3 ) {
89
226
return options ;
90
227
}
91
228
try {
92
- const res = await getMemberSuggestions ( inputVal ) ;
93
- const users = res . data . result . content ;
229
+ const res = await getMemberSuggestions ( inputValue ) ;
230
+ const users = res . data . result . content . slice ( 0 , 100 ) ;
231
+ let match = null ;
94
232
for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
95
233
let value = users [ i ] . handle ;
96
- options . push ( { value, label : value } ) ;
234
+ if ( value === inputValue ) {
235
+ match = { value, label : value } ;
236
+ } else {
237
+ options . push ( { value, label : value } ) ;
238
+ }
239
+ }
240
+ if ( match ) {
241
+ options . unshift ( match ) ;
97
242
}
98
243
} catch ( error ) {
99
244
console . error ( error ) ;
@@ -108,6 +253,8 @@ SearchHandleField.propTypes = {
108
253
size : PT . oneOf ( [ "medium" , "small" ] ) ,
109
254
name : PT . string . isRequired ,
110
255
onChange : PT . func . isRequired ,
256
+ onInputChange : PT . func ,
257
+ onBlur : PT . func ,
111
258
placeholder : PT . string ,
112
259
value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
113
260
} ;
0 commit comments