@@ -18,7 +18,7 @@ import {
1818 FilterMatchEnum ,
1919 OptionIndexEnum ,
2020 MenuPositionEnum ,
21- FunctionDefaults ,
21+ FUNCTION_DEFAULTS ,
2222 EMPTY_ARRAY ,
2323 DEFAULT_THEME ,
2424 SELECT_WRAPPER_ATTRS ,
@@ -31,11 +31,6 @@ import {
3131 MENU_MAX_HEIGHT_DEFAULT ,
3232 CONTROL_CONTAINER_TESTID
3333} from './constants' ;
34- import type { FixedSizeList } from 'react-window' ;
35- import styled , { css , ThemeProvider , type DefaultTheme } from 'styled-components' ;
36- import { Menu , Value , AriaLiveRegion , AutosizeInput , IndicatorIcons } from './components' ;
37- import { useDebounce , useCallbackRef , useMenuOptions , useMountEffect , useUpdateEffect , useMenuPositioner } from './hooks' ;
38- import { isBoolean , isFunction , isPlainObject , mergeDeep , suppressEvent , normalizeValue , IS_TOUCH_DEVICE , isArrayWithLength } from './utils' ;
3934import type {
4035 Theme ,
4136 SelectRef ,
@@ -52,6 +47,11 @@ import type {
5247 OptionValueCallback ,
5348 RenderLabelCallback
5449} from './types' ;
50+ import type { FixedSizeList } from 'react-window' ;
51+ import styled , { css , ThemeProvider , type DefaultTheme } from 'styled-components' ;
52+ import { Menu , Value , AriaLiveRegion , AutosizeInput , IndicatorIcons } from './components' ;
53+ import { useDebounce , useLatestRef , useCallbackRef , useMenuOptions , useMountEffect , useUpdateEffect , useMenuPositioner } from './hooks' ;
54+ import { isBoolean , isFunction , isPlainObject , mergeDeep , suppressEvent , normalizeValue , IS_TOUCH_DEVICE , isArrayWithLength } from './utils' ;
5555
5656type SelectProps = Readonly < {
5757 async ?: boolean ;
@@ -237,13 +237,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
237237 } ,
238238 ref : Ref < SelectRef >
239239) => {
240- // Instance prop refs (primitive/function type)
241- const menuOpenRef = useRef < boolean > ( false ) ;
242- const prevMenuOptionsLength = useRef < number > ( ) ;
243- const onChangeEvtValue = useRef < boolean > ( false ) ;
244- const onSearchChangeIsFunc = useRef < boolean > ( isFunction ( onSearchChange ) ) ;
245- const onOptionChangeIsFunc = useRef < boolean > ( isFunction ( onOptionChange ) ) ;
246-
247240 // DOM element refs
248241 const listRef = useRef < FixedSizeList | null > ( null ) ;
249242 const menuRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -265,13 +258,22 @@ const Select = forwardRef<SelectRef, SelectProps>((
265258 } , [ themeConfig ] ) ;
266259
267260 // Memoized callback functions referencing optional function properties on Select.tsx
268- const getOptionLabelFn = useMemo < OptionLabelCallback > ( ( ) => getOptionLabel || FunctionDefaults . OPTION_LABEL , [ getOptionLabel ] ) ;
269- const getOptionValueFn = useMemo < OptionValueCallback > ( ( ) => getOptionValue || FunctionDefaults . OPTION_VALUE , [ getOptionValue ] ) ;
261+ const getOptionLabelFn = useMemo < OptionLabelCallback > ( ( ) => getOptionLabel || FUNCTION_DEFAULTS . optionLabel , [ getOptionLabel ] ) ;
262+ const getOptionValueFn = useMemo < OptionValueCallback > ( ( ) => getOptionValue || FUNCTION_DEFAULTS . optionValue , [ getOptionValue ] ) ;
270263 const renderOptionLabelFn = useMemo < RenderLabelCallback > ( ( ) => renderOptionLabel || getOptionLabelFn , [ renderOptionLabel , getOptionLabelFn ] ) ;
271264
272265 // Custom hook abstraction that debounces search input value (opt-in)
273266 const debouncedInputValue = useDebounce < string > ( inputValue , inputDelay ) ;
274267
268+ // Custom ref objects
269+ const onSearchChangeRef = useCallbackRef ( onSearchChange ) ;
270+ const onOptionChangeRef = useCallbackRef ( onOptionChange ) ;
271+ const onSearchChangeIsFunc = useLatestRef < boolean > ( isFunction ( onSearchChange ) ) ;
272+ const onOptionChangeIsFunc = useLatestRef < boolean > ( isFunction ( onOptionChange ) ) ;
273+ const menuOpenRef = useLatestRef < boolean > ( menuOpen ) ;
274+ const onChangeEvtValue = useRef < boolean > ( false ) ;
275+ const prevMenuOptionsLength = useRef < number > ( ) ;
276+
275277 // If initialValue is specified attempt to initialize, otherwise default to []
276278 const [ selectedOption , setSelectedOption ] = useState < SelectedOption [ ] > (
277279 ( ) => normalizeValue (
@@ -314,9 +316,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
314316 scrollMenuIntoView ,
315317 ) ;
316318
317- const onSearchChangeRef = useCallbackRef ( onSearchChange ) ;
318- const onOptionChangeRef = useCallbackRef ( onOptionChange ) ;
319-
320319 const blurInput = ( ) : void => inputRef . current ?. blur ( ) ;
321320 const focusInput = ( ) : void => inputRef . current ?. focus ( ) ;
322321 const scrollToItemIndex = ( idx : number ) : void => listRef . current ?. scrollToItem ( idx ) ;
@@ -386,8 +385,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
386385 setFocusedOption ( FOCUSED_OPTION_DEFAULT ) ;
387386 } ,
388387 setValue : ( option ?: OptionData ) => {
389- const normalizedOptions = normalizeValue ( option , getOptionValueFn , getOptionLabelFn ) ;
390- setSelectedOption ( normalizedOptions ) ;
388+ const normalizedOpts = normalizeValue ( option , getOptionValueFn , getOptionLabelFn ) ;
389+ setSelectedOption ( normalizedOpts ) ;
391390 } ,
392391 toggleMenu : ( state ?: boolean ) => {
393392 if ( state === true || ( state === undefined && ! menuOpenRef . current ) ) {
@@ -402,31 +401,15 @@ const Select = forwardRef<SelectRef, SelectProps>((
402401 ) ;
403402
404403 /**
405- * useMountEffect:
404+ * useMountEffect
406405 * If autoFocus = true, focus the control following initial mount.
407406 */
408407 useMountEffect ( ( ) => {
409408 autoFocus && focusInput ( ) ;
410409 } ) ;
411410
412411 /**
413- * Execute every render - these ref boolean flags are used to determine if functions
414- * ..are defined inside of a callback wrapper returned from 'useCallbackRef' custom hook
415- */
416- useEffect ( ( ) => {
417- onSearchChangeIsFunc . current = isFunction ( onSearchChange ) ;
418- onOptionChangeIsFunc . current = isFunction ( onOptionChange ) ;
419- } ) ;
420-
421- /**
422- * Write value of 'menuOpen' to ref object.
423- * Prevent extraneous state update calls/rerenders.
424- */
425- useEffect ( ( ) => {
426- menuOpenRef . current = menuOpen ;
427- } , [ menuOpen ] ) ;
428-
429- /**
412+ * useEffect
430413 * If control recieves focus & openMenuOnFocus = true, open menu
431414 */
432415 useEffect ( ( ) => {
@@ -436,6 +419,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
436419 } , [ isFocused , openMenuOnFocus , openMenuAndFocusOption ] ) ;
437420
438421 /**
422+ * useEffect
439423 * If 'onSearchChange' function is defined, run as callback when the stateful debouncedInputValue
440424 * updates check if onChangeEvtValue ref is set true, which indicates the inputValue change was triggered by input change event
441425 */
@@ -447,7 +431,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
447431 } , [ onSearchChangeRef , debouncedInputValue ] ) ;
448432
449433 /**
450- * useUpdateEffect:
434+ * useUpdateEffect
451435 * Handle passing 'selectedOption' value(s) to onOptionChange callback function prop (if defined)
452436 */
453437 useUpdateEffect ( ( ) => {
@@ -463,23 +447,27 @@ const Select = forwardRef<SelectRef, SelectProps>((
463447 } , [ onOptionChangeRef , isMulti , selectedOption ] ) ;
464448
465449 /**
466- * useUpdateEffect:
450+ * useUpdateEffect
467451 * Handle clearing focused option if menuOptions array has 0 length;
468452 * Handle menuOptions changes - conditionally focus first option and do scroll to first option;
469453 * Handle reseting scroll pos to first item after the previous search returned zero results (use prevMenuOptionsLen)
454+ * ...or if there is a selected item and menuOptions is restored to include it, give it focus
470455 */
471456 useUpdateEffect ( ( ) => {
472- const { length } = menuOptions ;
473- const inputChanged = length > 0 && ( async || length !== options . length || prevMenuOptionsLength . current === 0 ) ;
457+ const curLength = menuOptions . length ;
458+ const { current : prevLength } = prevMenuOptionsLength ;
459+ const inputChanged = curLength > 0 && ( async || curLength !== options . length || prevLength === 0 ) ;
460+ const menuOpenAndOptionsGrew = menuOpenRef . current && prevLength !== undefined && prevLength < curLength ;
474461
475- if ( length === 0 ) {
462+ if ( curLength === 0 ) {
476463 setFocusedOption ( FOCUSED_OPTION_DEFAULT ) ;
477- } else if ( length === 1 || inputChanged ) {
478- scrollToItemIndex ( 0 ) ;
479- setFocusedOption ( { index : 0 , ...menuOptions [ 0 ] } ) ;
464+ } else if ( curLength === 1 || inputChanged || menuOpenAndOptionsGrew ) {
465+ const index = Math . max ( 0 , menuOptions . findIndex ( ( x ) => x . isSelected ) ) ;
466+ scrollToItemIndex ( index ) ;
467+ setFocusedOption ( { index, ...menuOptions [ index ] } ) ;
480468 }
481469
482- prevMenuOptionsLength . current = length ;
470+ prevMenuOptionsLength . current = curLength ;
483471 } , [ async , options , menuOptions ] ) ;
484472
485473 const selectOptionFromFocused = ( ) : void => {
@@ -543,11 +531,15 @@ const Select = forwardRef<SelectRef, SelectProps>((
543531
544532 switch ( key ) {
545533 case 'ArrowDown' : {
546- menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
534+ menuOpen
535+ ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN )
536+ : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
547537 break ;
548538 }
549539 case 'ArrowUp' : {
550- menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . UP ) : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
540+ menuOpen
541+ ? focusOptionOnArrowKey ( OptionIndexEnum . UP )
542+ : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
551543 break ;
552544 }
553545 case 'ArrowLeft' :
@@ -636,7 +628,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
636628 if ( ! isFocused ) focusInput ( ) ;
637629
638630 const isNotInput = ( e . target as HTMLElement ) . nodeName !== 'INPUT' ;
639-
640631 if ( ! menuOpen ) {
641632 openMenuOnClick && openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
642633 } else if ( isNotInput ) {
@@ -661,9 +652,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
661652
662653 const handleOnInputChange = useCallback ( ( e : FormEvent < HTMLInputElement > ) : void => {
663654 onChangeEvtValue . current = true ;
664- const curVal = e . currentTarget . value ;
665- onInputChange ?.( curVal ) ;
666- setInputValue ( curVal ) ;
655+ onInputChange ?.( e . currentTarget . value ) ;
656+ setInputValue ( e . currentTarget . value ) ;
667657 setMenuOpen ( true ) ;
668658 } , [ onInputChange ] ) ;
669659
0 commit comments