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
+
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
+
8
47
const selectComponents = {
9
48
DropdownIndicator : ( ) => null ,
49
+ ClearIndicator : ( ) => null ,
10
50
IndicatorSeparator : ( ) => null ,
51
+ MenuList,
52
+ Option,
11
53
} ;
12
54
13
- const loadingMessage = ( ) => "Loading..." ;
14
-
15
- const noOptionsMessage = ( ) => "No suggestions" ;
16
-
17
55
/**
18
56
* Displays search input field.
19
57
*
@@ -23,10 +61,9 @@ const noOptionsMessage = () => "No suggestions";
23
61
* @param {string } props.placeholder placeholder text
24
62
* @param {string } props.name name for input element
25
63
* @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
27
65
* @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
30
67
* @param {string } props.value input value
31
68
* @returns {JSX.Element }
32
69
*/
@@ -38,70 +75,139 @@ const SearchHandleField = ({
38
75
onChange,
39
76
onInputChange,
40
77
onBlur,
41
- onMenuClose,
42
78
placeholder,
43
79
value,
44
80
} ) => {
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 ) {
50
110
onChange ( option . value ) ;
51
111
}
52
- } ,
53
- [ onChange ]
54
- ) ;
112
+ } else if ( action === "clear" ) {
113
+ setIsMenuFocused ( false ) ;
114
+ setIsMenuOpen ( false ) ;
115
+ onChange ( "" ) ;
116
+ }
117
+ } ;
55
118
56
119
const onInputValueChange = useCallback (
57
120
( value , { action } ) => {
58
121
if ( action === "input-change" ) {
59
- onInputChange ( value ) ;
122
+ setIsMenuFocused ( false ) ;
123
+ setInputValue ( value ) ;
124
+ onInputChange && onInputChange ( value ) ;
125
+ loadOptions ( value ) ;
60
126
}
61
127
} ,
62
- [ onInputChange ]
128
+ [ onInputChange , loadOptions ]
63
129
) ;
64
130
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
+
65
163
return (
66
164
< div className = { cn ( styles . container , styles [ size ] , className ) } >
67
165
< span className = { styles . icon } />
68
- < AsyncSelect
166
+ < Select
69
167
className = { styles . select }
70
168
classNamePrefix = "custom"
71
169
components = { selectComponents }
72
170
id = { id }
73
171
name = { name }
74
172
isClearable = { true }
75
173
isSearchable = { true }
76
- // menuIsOpen={true} // for debugging
174
+ isLoading = { isLoading }
175
+ isMenuFocused = { isMenuFocused }
176
+ menuIsOpen = { isMenuOpen }
77
177
value = { null }
78
- inputValue = { value }
178
+ inputValue = { inputValue }
179
+ options = { options }
79
180
onChange = { onValueChange }
80
181
onInputChange = { onInputValueChange }
81
- onBlur = { onBlur }
82
- onMenuClose = { onMenuClose }
83
- openMenuOnClick = { false }
182
+ onKeyDown = { onKeyDown }
183
+ onBlur = { onSelectBlur }
84
184
placeholder = { placeholder }
85
185
noOptionsMessage = { noOptionsMessage }
86
186
loadingMessage = { loadingMessage }
87
- loadOptions = { loadSuggestions }
88
- cacheOptions
89
187
/>
90
188
</ div >
91
189
) ;
92
190
} ;
93
191
94
- const loadSuggestions = async ( inputVal ) => {
192
+ const loadSuggestions = async ( inputValue ) => {
95
193
let options = [ ] ;
96
- if ( inputVal . length < 3 ) {
194
+ if ( inputValue . length < 3 ) {
97
195
return options ;
98
196
}
99
197
try {
100
- const res = await getMemberSuggestions ( inputVal ) ;
198
+ const res = await getMemberSuggestions ( inputValue ) ;
101
199
const users = res . data . result . content ;
200
+ let match = null ;
102
201
for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
103
202
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 ) ;
105
211
}
106
212
} catch ( error ) {
107
213
console . error ( error ) ;
@@ -118,7 +224,6 @@ SearchHandleField.propTypes = {
118
224
onChange : PT . func . isRequired ,
119
225
onInputChange : PT . func ,
120
226
onBlur : PT . func ,
121
- onMenuClose : PT . func ,
122
227
placeholder : PT . string ,
123
228
value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
124
229
} ;
0 commit comments