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+ 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,149 @@ 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+
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 ) {
44120 onChange ( option . value ) ;
45121 }
46- } ,
47- [ onChange ]
48- ) ;
122+ } else if ( action === "clear" ) {
123+ setIsMenuFocused ( false ) ;
124+ setIsMenuOpen ( false ) ;
125+ onChange ( "" ) ;
126+ }
127+ } ;
49128
50- const onInputChange = useCallback (
129+ const onInputValueChange = useCallback (
51130 ( value , { action } ) => {
52131 if ( action === "input-change" ) {
53- onChange ( value ) ;
132+ setIsMenuFocused ( false ) ;
133+ setInputValue ( value ) ;
134+ onInputChange && onInputChange ( value ) ;
135+ loadOptions ( value ) ;
54136 }
55137 } ,
56- [ onChange ]
138+ [ onInputChange , loadOptions ]
57139 ) ;
58140
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+
59173 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+ >
61182 < span className = { styles . icon } />
62- < AsyncSelect
183+ < Select
63184 className = { styles . select }
64185 classNamePrefix = "custom"
65186 components = { selectComponents }
66187 id = { id }
67188 name = { name }
68189 isClearable = { true }
69190 isSearchable = { true }
70- // menuIsOpen={true} // for debugging
191+ isLoading = { isLoading }
192+ isMenuFocused = { isMenuFocused }
193+ setIsMenuFocused = { setIsMenuFocused }
194+ menuIsOpen = { isMenuOpen }
71195 value = { null }
72- inputValue = { value }
196+ inputValue = { inputValue }
197+ options = { options }
73198 onChange = { onValueChange }
74- onInputChange = { onInputChange }
75- openMenuOnClick = { false }
199+ onInputChange = { onInputValueChange }
200+ onKeyDown = { onKeyDown }
201+ onBlur = { onSelectBlur }
76202 placeholder = { placeholder }
77203 noOptionsMessage = { noOptionsMessage }
78204 loadingMessage = { loadingMessage }
79- loadOptions = { loadSuggestions }
80- cacheOptions
81205 />
82206 </ div >
83207 ) ;
84208} ;
85209
86- const loadSuggestions = async ( inputVal ) => {
210+ const loadSuggestions = async ( inputValue ) => {
87211 let options = [ ] ;
88- if ( inputVal . length < 3 ) {
212+ if ( inputValue . length < 3 ) {
89213 return options ;
90214 }
91215 try {
92- const res = await getMemberSuggestions ( inputVal ) ;
216+ const res = await getMemberSuggestions ( inputValue ) ;
93217 const users = res . data . result . content ;
218+ let match = null ;
94219 for ( let i = 0 , len = users . length ; i < len ; i ++ ) {
95220 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 ) ;
97229 }
98230 } catch ( error ) {
99231 console . error ( error ) ;
@@ -108,6 +240,8 @@ SearchHandleField.propTypes = {
108240 size : PT . oneOf ( [ "medium" , "small" ] ) ,
109241 name : PT . string . isRequired ,
110242 onChange : PT . func . isRequired ,
243+ onInputChange : PT . func ,
244+ onBlur : PT . func ,
111245 placeholder : PT . string ,
112246 value : PT . oneOfType ( [ PT . number , PT . string ] ) ,
113247} ;
0 commit comments