1- import React , { useCallback } from "react" ;
1+ import React , { useCallback , useRef , 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+ 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+
857const selectComponents = {
958 DropdownIndicator : ( ) => null ,
59+ ClearIndicator : ( ) => null ,
1060 IndicatorSeparator : ( ) => null ,
61+ MenuList,
62+ Option,
1163} ;
1264
13- const loadingMessage = ( ) => "Loading..." ;
14-
15- const noOptionsMessage = ( ) => "No suggestions" ;
16-
1765/**
1866 * Displays search input field.
1967 *
@@ -23,7 +71,9 @@ const noOptionsMessage = () => "No suggestions";
2371 * @param {string } props.placeholder placeholder text
2472 * @param {string } props.name name for input element
2573 * @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
2777 * @param {string } props.value input value
2878 * @returns {JSX.Element }
2979 */
@@ -33,67 +83,162 @@ const SearchHandleField = ({
3383 name,
3484 size = "medium" ,
3585 onChange,
86+ onInputChange,
87+ onBlur,
3688 placeholder,
3789 value,
3890} ) => {
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 ) ;
44105 onChange ( option . value ) ;
45106 }
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+ } ;
49115
50- const onInputChange = useCallback (
116+ const onInputValueChange = useCallback (
51117 ( value , { action } ) => {
52118 if ( action === "input-change" ) {
53- onChange ( value ) ;
119+ isChangeAppliedRef . current = false ;
120+ setIsMenuFocused ( false ) ;
121+ setInputValue ( value ) ;
122+ onInputChange && onInputChange ( value ) ;
54123 }
55124 } ,
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+ [ ]
57176 ) ;
58177
178+ useUpdateEffect ( ( ) => {
179+ setInputValue ( value ) ;
180+ } , [ value ] ) ;
181+
182+ useUpdateEffect ( ( ) => {
183+ loadOptions ( inputValue ) ;
184+ } , [ inputValue ] ) ;
185+
59186 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+ >
61195 < span className = { styles . icon } />
62- < AsyncSelect
196+ < Select
63197 className = { styles . select }
64198 classNamePrefix = "custom"
65199 components = { selectComponents }
66200 id = { id }
67201 name = { name }
68202 isClearable = { true }
69203 isSearchable = { true }
70- // menuIsOpen={true} // for debugging
204+ isLoading = { isLoading }
205+ isMenuFocused = { isMenuFocused }
206+ setIsMenuFocused = { setIsMenuFocused }
207+ menuIsOpen = { isMenuOpen }
71208 value = { null }
72- inputValue = { value }
209+ inputValue = { inputValue }
210+ options = { options }
73211 onChange = { onValueChange }
74- onInputChange = { onInputChange }
75- openMenuOnClick = { false }
212+ onInputChange = { onInputValueChange }
213+ onKeyDown = { onKeyDown }
214+ onBlur = { onSelectBlur }
76215 placeholder = { placeholder }
77216 noOptionsMessage = { noOptionsMessage }
78217 loadingMessage = { loadingMessage }
79- loadOptions = { loadSuggestions }
80- cacheOptions
81218 />
82219 </ div >
83220 ) ;
84221} ;
85222
86- const loadSuggestions = async ( inputVal ) => {
223+ const loadSuggestions = async ( inputValue ) => {
87224 let options = [ ] ;
88- if ( inputVal . length < 3 ) {
225+ if ( inputValue . length < 3 ) {
89226 return options ;
90227 }
91228 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 ;
94232 for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
95233 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 ) ;
97242 }
98243 } catch ( error ) {
99244 console . error ( error ) ;
@@ -108,6 +253,8 @@ SearchHandleField.propTypes = {
108253 size : PT . oneOf ( [ "medium" , "small" ] ) ,
109254 name : PT . string . isRequired ,
110255 onChange : PT . func . isRequired ,
256+ onInputChange : PT . func ,
257+ onBlur : PT . func ,
111258 placeholder : PT . string ,
112259 value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
113260} ;
0 commit comments