@@ -18,7 +18,7 @@ import {
18
18
FilterMatchEnum ,
19
19
OptionIndexEnum ,
20
20
MenuPositionEnum ,
21
- FunctionDefaults ,
21
+ FUNCTION_DEFAULTS ,
22
22
EMPTY_ARRAY ,
23
23
DEFAULT_THEME ,
24
24
SELECT_WRAPPER_ATTRS ,
@@ -31,11 +31,6 @@ import {
31
31
MENU_MAX_HEIGHT_DEFAULT ,
32
32
CONTROL_CONTAINER_TESTID
33
33
} 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' ;
39
34
import type {
40
35
Theme ,
41
36
SelectRef ,
@@ -52,6 +47,11 @@ import type {
52
47
OptionValueCallback ,
53
48
RenderLabelCallback
54
49
} 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' ;
55
55
56
56
type SelectProps = Readonly < {
57
57
async ?: boolean ;
@@ -237,13 +237,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
237
237
} ,
238
238
ref : Ref < SelectRef >
239
239
) => {
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
-
247
240
// DOM element refs
248
241
const listRef = useRef < FixedSizeList | null > ( null ) ;
249
242
const menuRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -265,13 +258,22 @@ const Select = forwardRef<SelectRef, SelectProps>((
265
258
} , [ themeConfig ] ) ;
266
259
267
260
// 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 ] ) ;
270
263
const renderOptionLabelFn = useMemo < RenderLabelCallback > ( ( ) => renderOptionLabel || getOptionLabelFn , [ renderOptionLabel , getOptionLabelFn ] ) ;
271
264
272
265
// Custom hook abstraction that debounces search input value (opt-in)
273
266
const debouncedInputValue = useDebounce < string > ( inputValue , inputDelay ) ;
274
267
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
+
275
277
// If initialValue is specified attempt to initialize, otherwise default to []
276
278
const [ selectedOption , setSelectedOption ] = useState < SelectedOption [ ] > (
277
279
( ) => normalizeValue (
@@ -314,9 +316,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
314
316
scrollMenuIntoView ,
315
317
) ;
316
318
317
- const onSearchChangeRef = useCallbackRef ( onSearchChange ) ;
318
- const onOptionChangeRef = useCallbackRef ( onOptionChange ) ;
319
-
320
319
const blurInput = ( ) : void => inputRef . current ?. blur ( ) ;
321
320
const focusInput = ( ) : void => inputRef . current ?. focus ( ) ;
322
321
const scrollToItemIndex = ( idx : number ) : void => listRef . current ?. scrollToItem ( idx ) ;
@@ -386,8 +385,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
386
385
setFocusedOption ( FOCUSED_OPTION_DEFAULT ) ;
387
386
} ,
388
387
setValue : ( option ?: OptionData ) => {
389
- const normalizedOptions = normalizeValue ( option , getOptionValueFn , getOptionLabelFn ) ;
390
- setSelectedOption ( normalizedOptions ) ;
388
+ const normalizedOpts = normalizeValue ( option , getOptionValueFn , getOptionLabelFn ) ;
389
+ setSelectedOption ( normalizedOpts ) ;
391
390
} ,
392
391
toggleMenu : ( state ?: boolean ) => {
393
392
if ( state === true || ( state === undefined && ! menuOpenRef . current ) ) {
@@ -402,31 +401,15 @@ const Select = forwardRef<SelectRef, SelectProps>((
402
401
) ;
403
402
404
403
/**
405
- * useMountEffect:
404
+ * useMountEffect
406
405
* If autoFocus = true, focus the control following initial mount.
407
406
*/
408
407
useMountEffect ( ( ) => {
409
408
autoFocus && focusInput ( ) ;
410
409
} ) ;
411
410
412
411
/**
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
430
413
* If control recieves focus & openMenuOnFocus = true, open menu
431
414
*/
432
415
useEffect ( ( ) => {
@@ -436,6 +419,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
436
419
} , [ isFocused , openMenuOnFocus , openMenuAndFocusOption ] ) ;
437
420
438
421
/**
422
+ * useEffect
439
423
* If 'onSearchChange' function is defined, run as callback when the stateful debouncedInputValue
440
424
* updates check if onChangeEvtValue ref is set true, which indicates the inputValue change was triggered by input change event
441
425
*/
@@ -447,7 +431,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
447
431
} , [ onSearchChangeRef , debouncedInputValue ] ) ;
448
432
449
433
/**
450
- * useUpdateEffect:
434
+ * useUpdateEffect
451
435
* Handle passing 'selectedOption' value(s) to onOptionChange callback function prop (if defined)
452
436
*/
453
437
useUpdateEffect ( ( ) => {
@@ -463,23 +447,27 @@ const Select = forwardRef<SelectRef, SelectProps>((
463
447
} , [ onOptionChangeRef , isMulti , selectedOption ] ) ;
464
448
465
449
/**
466
- * useUpdateEffect:
450
+ * useUpdateEffect
467
451
* Handle clearing focused option if menuOptions array has 0 length;
468
452
* Handle menuOptions changes - conditionally focus first option and do scroll to first option;
469
453
* 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
470
455
*/
471
456
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 ;
474
461
475
- if ( length === 0 ) {
462
+ if ( curLength === 0 ) {
476
463
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 ] } ) ;
480
468
}
481
469
482
- prevMenuOptionsLength . current = length ;
470
+ prevMenuOptionsLength . current = curLength ;
483
471
} , [ async , options , menuOptions ] ) ;
484
472
485
473
const selectOptionFromFocused = ( ) : void => {
@@ -543,11 +531,15 @@ const Select = forwardRef<SelectRef, SelectProps>((
543
531
544
532
switch ( key ) {
545
533
case 'ArrowDown' : {
546
- menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
534
+ menuOpen
535
+ ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN )
536
+ : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
547
537
break ;
548
538
}
549
539
case 'ArrowUp' : {
550
- menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . UP ) : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
540
+ menuOpen
541
+ ? focusOptionOnArrowKey ( OptionIndexEnum . UP )
542
+ : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
551
543
break ;
552
544
}
553
545
case 'ArrowLeft' :
@@ -636,7 +628,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
636
628
if ( ! isFocused ) focusInput ( ) ;
637
629
638
630
const isNotInput = ( e . target as HTMLElement ) . nodeName !== 'INPUT' ;
639
-
640
631
if ( ! menuOpen ) {
641
632
openMenuOnClick && openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
642
633
} else if ( isNotInput ) {
@@ -661,9 +652,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
661
652
662
653
const handleOnInputChange = useCallback ( ( e : FormEvent < HTMLInputElement > ) : void => {
663
654
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 ) ;
667
657
setMenuOpen ( true ) ;
668
658
} , [ onInputChange ] ) ;
669
659
0 commit comments