1
- import React , { useCallback } from "react" ;
1
+ import React , { useCallback , 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,149 @@ 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
+
97
+ const loadOptions = useCallback (
98
+ throttle (
99
+ async ( value ) => {
100
+ setIsLoading ( true ) ;
101
+ const options = await loadSuggestions ( value ) ;
102
+ setOptions ( options ) ;
103
+ setIsLoading ( false ) ;
104
+ setIsMenuOpen ( true ) ;
105
+ setIsMenuFocused ( options . length && options [ 0 ] . value === value ) ;
106
+ } ,
107
+ 300 ,
108
+ { leading : false }
109
+ ) ,
110
+ [ ]
111
+ ) ;
112
+
113
+ const onValueChange = ( option , { action } ) => {
114
+ if ( action === "input-change" || action === "select-option" ) {
115
+ setIsMenuFocused ( false ) ;
116
+ setIsMenuOpen ( false ) ;
117
+ if ( ! isMenuFocused ) {
118
+ onChange ( inputValue ) ;
119
+ } else if ( option ) {
44
120
onChange ( option . value ) ;
45
121
}
46
- } ,
47
- [ onChange ]
48
- ) ;
122
+ } else if ( action === "clear" ) {
123
+ setIsMenuFocused ( false ) ;
124
+ setIsMenuOpen ( false ) ;
125
+ onChange ( "" ) ;
126
+ }
127
+ } ;
49
128
50
- const onInputChange = useCallback (
129
+ const onInputValueChange = useCallback (
51
130
( value , { action } ) => {
52
131
if ( action === "input-change" ) {
53
- onChange ( value ) ;
132
+ setIsMenuFocused ( false ) ;
133
+ setInputValue ( value ) ;
134
+ onInputChange && onInputChange ( value ) ;
135
+ loadOptions ( value ) ;
54
136
}
55
137
} ,
56
- [ onChange ]
138
+ [ onInputChange , loadOptions ]
57
139
) ;
58
140
141
+ const onKeyDown = ( event ) => {
142
+ const key = event . key ;
143
+ if ( key === "Enter" || key === "Escape" ) {
144
+ setIsMenuFocused ( false ) ;
145
+ setIsMenuOpen ( false ) ;
146
+ if ( ! isMenuFocused ) {
147
+ onChange ( inputValue ) ;
148
+ }
149
+ } else if ( key === "ArrowDown" ) {
150
+ if ( ! isMenuFocused ) {
151
+ event . preventDefault ( ) ;
152
+ event . stopPropagation ( ) ;
153
+ setIsMenuFocused ( true ) ;
154
+ }
155
+ } else if ( key === "Backspace" ) {
156
+ if ( ! inputValue ) {
157
+ event . preventDefault ( ) ;
158
+ event . stopPropagation ( ) ;
159
+ }
160
+ }
161
+ } ;
162
+
163
+ const onSelectBlur = useCallback ( ( ) => {
164
+ setIsMenuFocused ( false ) ;
165
+ setIsMenuOpen ( false ) ;
166
+ onBlur && onBlur ( ) ;
167
+ } , [ onBlur ] ) ;
168
+
169
+ useUpdateEffect ( ( ) => {
170
+ setInputValue ( value ) ;
171
+ } , [ value ] ) ;
172
+
59
173
return (
60
- < div className = { cn ( styles . container , styles [ size ] , className ) } >
174
+ < div
175
+ className = { cn (
176
+ styles . container ,
177
+ styles [ size ] ,
178
+ { [ styles . isMenuFocused ] : isMenuFocused } ,
179
+ className
180
+ ) }
181
+ >
61
182
< span className = { styles . icon } />
62
- < AsyncSelect
183
+ < Select
63
184
className = { styles . select }
64
185
classNamePrefix = "custom"
65
186
components = { selectComponents }
66
187
id = { id }
67
188
name = { name }
68
189
isClearable = { true }
69
190
isSearchable = { true }
70
- // menuIsOpen={true} // for debugging
191
+ isLoading = { isLoading }
192
+ isMenuFocused = { isMenuFocused }
193
+ setIsMenuFocused = { setIsMenuFocused }
194
+ menuIsOpen = { isMenuOpen }
71
195
value = { null }
72
- inputValue = { value }
196
+ inputValue = { inputValue }
197
+ options = { options }
73
198
onChange = { onValueChange }
74
- onInputChange = { onInputChange }
75
- openMenuOnClick = { false }
199
+ onInputChange = { onInputValueChange }
200
+ onKeyDown = { onKeyDown }
201
+ onBlur = { onSelectBlur }
76
202
placeholder = { placeholder }
77
203
noOptionsMessage = { noOptionsMessage }
78
204
loadingMessage = { loadingMessage }
79
- loadOptions = { loadSuggestions }
80
- cacheOptions
81
205
/>
82
206
</ div >
83
207
) ;
84
208
} ;
85
209
86
- const loadSuggestions = async ( inputVal ) => {
210
+ const loadSuggestions = async ( inputValue ) => {
87
211
let options = [ ] ;
88
- if ( inputVal . length < 3 ) {
212
+ if ( inputValue . length < 3 ) {
89
213
return options ;
90
214
}
91
215
try {
92
- const res = await getMemberSuggestions ( inputVal ) ;
216
+ const res = await getMemberSuggestions ( inputValue ) ;
93
217
const users = res . data . result . content ;
218
+ let match = null ;
94
219
for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
95
220
let value = users [ i ] . handle ;
96
- options . push ( { value, label : value } ) ;
221
+ if ( value === inputValue ) {
222
+ match = { value, label : value } ;
223
+ } else {
224
+ options . push ( { value, label : value } ) ;
225
+ }
226
+ }
227
+ if ( match ) {
228
+ options . unshift ( match ) ;
97
229
}
98
230
} catch ( error ) {
99
231
console . error ( error ) ;
@@ -108,6 +240,8 @@ SearchHandleField.propTypes = {
108
240
size : PT . oneOf ( [ "medium" , "small" ] ) ,
109
241
name : PT . string . isRequired ,
110
242
onChange : PT . func . isRequired ,
243
+ onInputChange : PT . func ,
244
+ onBlur : PT . func ,
111
245
placeholder : PT . string ,
112
246
value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
113
247
} ;
0 commit comments