@@ -18,7 +18,7 @@ import {
18
18
FilterMatchEnum ,
19
19
OptionIndexEnum ,
20
20
MenuPositionEnum ,
21
- FUNCTION_DEFAULTS ,
21
+ FUNCTIONS ,
22
22
EMPTY_ARRAY ,
23
23
DEFAULT_THEME ,
24
24
SELECT_WRAPPER_ATTRS ,
@@ -51,11 +51,12 @@ import type { FixedSizeList } from 'react-window';
51
51
import styled , { css , ThemeProvider , type DefaultTheme } from 'styled-components' ;
52
52
import { Menu , Value , AriaLiveRegion , AutosizeInput , IndicatorIcons } from './components' ;
53
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' ;
54
+ import { isBoolean , isFunction , isPlainObject , mergeDeep , suppressEvent , normalizeValue , isTouchDevice , isArrayWithLength } from './utils' ;
55
55
56
56
type SelectProps = Readonly < {
57
57
async ?: boolean ;
58
58
inputId ?: string ;
59
+ pageSize : number ;
59
60
selectId ?: string ;
60
61
isMulti ?: boolean ;
61
62
ariaLabel ?: string ;
@@ -217,6 +218,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
217
218
hideSelectedOptions,
218
219
getIsOptionDisabled,
219
220
getFilterOptionString,
221
+ pageSize = 5 ,
220
222
isSearchable = true ,
221
223
memoOptions = false ,
222
224
lazyLoadMenu = false ,
@@ -258,8 +260,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
258
260
} , [ themeConfig ] ) ;
259
261
260
262
// Memoized callback functions referencing optional function properties on Select.tsx
261
- const getOptionLabelFn = useMemo < OptionLabelCallback > ( ( ) => getOptionLabel || FUNCTION_DEFAULTS . optionLabel , [ getOptionLabel ] ) ;
262
- const getOptionValueFn = useMemo < OptionValueCallback > ( ( ) => getOptionValue || FUNCTION_DEFAULTS . optionValue , [ getOptionValue ] ) ;
263
+ const getOptionLabelFn = useMemo < OptionLabelCallback > ( ( ) => getOptionLabel || FUNCTIONS . optionLabel , [ getOptionLabel ] ) ;
264
+ const getOptionValueFn = useMemo < OptionValueCallback > ( ( ) => getOptionValue || FUNCTIONS . optionValue , [ getOptionValue ] ) ;
263
265
const renderOptionLabelFn = useMemo < RenderLabelCallback > ( ( ) => renderOptionLabel || getOptionLabelFn , [ renderOptionLabel , getOptionLabelFn ] ) ;
264
266
265
267
// Custom hook abstraction that debounces search input value (opt-in)
@@ -319,10 +321,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
319
321
const blurInput = ( ) : void => inputRef . current ?. blur ( ) ;
320
322
const focusInput = ( ) : void => inputRef . current ?. focus ( ) ;
321
323
const scrollToItemIndex = ( idx : number ) : void => listRef . current ?. scrollToItem ( idx ) ;
322
-
323
- // Local boolean flags based on component props
324
324
const hasSelectedOptions = isArrayWithLength ( selectedOption ) ;
325
- const blurInputOnSelectOrDefault = isBoolean ( blurInputOnSelect ) ? blurInputOnSelect : IS_TOUCH_DEVICE ;
326
325
327
326
const openMenuAndFocusOption = useCallback ( ( position : OptionIndexEnum ) : void => {
328
327
if ( ! isArrayWithLength ( menuOptions ) ) {
@@ -360,13 +359,14 @@ const Select = forwardRef<SelectRef, SelectProps>((
360
359
setSelectedOption ( ( prev ) => ! isMulti ? [ selectedOpt ] : [ ...prev , selectedOpt ] ) ;
361
360
}
362
361
363
- if ( blurInputOnSelectOrDefault ) {
362
+ const blurOrDefault = isBoolean ( blurInputOnSelect ) ? blurInputOnSelect : isTouchDevice ( ) ;
363
+ if ( blurOrDefault ) {
364
364
blurInput ( ) ;
365
365
} else if ( closeMenuOnSelect ) {
366
366
setInputValue ( '' ) ;
367
367
setMenuOpen ( false ) ;
368
368
}
369
- } , [ isMulti , closeMenuOnSelect , removeSelectedOption , blurInputOnSelectOrDefault ] ) ;
369
+ } , [ isMulti , closeMenuOnSelect , blurInputOnSelect , removeSelectedOption ] ) ;
370
370
371
371
/**
372
372
* useImperativeHandle.
@@ -508,15 +508,30 @@ const Select = forwardRef<SelectRef, SelectProps>((
508
508
const focusOptionOnArrowKey = ( direction : OptionIndexEnum ) : void => {
509
509
if ( ! isArrayWithLength ( menuOptions ) ) return ;
510
510
511
- const index =
512
- direction === OptionIndexEnum . DOWN
513
- ? ( focusedOption . index + 1 ) % menuOptions . length
514
- : focusedOption . index > 0
515
- ? focusedOption . index - 1
516
- : menuOptions . length - 1 ;
511
+ let index = focusedOption . index ;
512
+ switch ( direction ) {
513
+ case OptionIndexEnum . UP : {
514
+ index = ( focusedOption . index > 0 ) ? focusedOption . index - 1 : menuOptions . length - 1 ;
515
+ break ;
516
+ }
517
+ case OptionIndexEnum . DOWN : {
518
+ index = ( focusedOption . index + 1 ) % menuOptions . length ;
519
+ break ;
520
+ }
521
+ case OptionIndexEnum . PAGEUP : {
522
+ const pageIndex = focusedOption . index - pageSize ;
523
+ index = ( pageIndex < 0 ) ? 0 : pageIndex ;
524
+ break ;
525
+ }
526
+ case OptionIndexEnum . PAGEDOWN : {
527
+ const pageIndex = focusedOption . index + pageSize ;
528
+ index = ( pageIndex > menuOptions . length - 1 ) ? menuOptions . length - 1 : pageIndex ;
529
+ break ;
530
+ }
531
+ }
517
532
518
533
scrollToItemIndex ( index ) ;
519
- setFocusedMultiValue ( null ) ;
534
+ focusedMultiValue && setFocusedMultiValue ( null ) ;
520
535
setFocusedOption ( { index, ...menuOptions [ index ] } ) ;
521
536
} ;
522
537
@@ -531,25 +546,29 @@ const Select = forwardRef<SelectRef, SelectProps>((
531
546
532
547
switch ( key ) {
533
548
case 'ArrowDown' : {
534
- menuOpen
535
- ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN )
536
- : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
549
+ menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
537
550
break ;
538
551
}
539
552
case 'ArrowUp' : {
540
- menuOpen
541
- ? focusOptionOnArrowKey ( OptionIndexEnum . UP )
542
- : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
553
+ menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . UP ) : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
543
554
break ;
544
555
}
545
556
case 'ArrowLeft' :
546
557
case 'ArrowRight' : {
547
- if ( ! isMulti || inputValue || renderMultiOptions ) {
548
- return ;
549
- }
558
+ if ( ! isMulti || inputValue || renderMultiOptions ) return ;
550
559
focusValueOnArrowKey ( key ) ;
551
560
break ;
552
561
}
562
+ case 'PageUp' : {
563
+ if ( ! menuOpen ) return ;
564
+ focusOptionOnArrowKey ( OptionIndexEnum . PAGEUP ) ;
565
+ break ;
566
+ }
567
+ case 'PageDown' : {
568
+ if ( ! menuOpen ) return ;
569
+ focusOptionOnArrowKey ( OptionIndexEnum . PAGEDOWN ) ;
570
+ break ;
571
+ }
553
572
// Handle spacebar keydown events
554
573
case ' ' : {
555
574
if ( inputValue ) return ;
@@ -565,9 +584,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
565
584
break ;
566
585
}
567
586
case 'Enter' : {
568
- if ( menuOpen ) {
569
- selectOptionFromFocused ( ) ;
570
- }
587
+ if ( ! menuOpen ) return ;
588
+ selectOptionFromFocused ( ) ;
571
589
break ;
572
590
}
573
591
case 'Escape' : {
@@ -578,9 +596,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
578
596
break ;
579
597
}
580
598
case 'Tab' : {
581
- if ( shiftKey || ! menuOpen || ! tabSelectsOption || ! focusedOption . data ) {
582
- return ;
583
- }
599
+ if ( shiftKey || ! menuOpen || ! tabSelectsOption || ! focusedOption . data ) return ;
584
600
selectOptionFromFocused ( ) ;
585
601
break ;
586
602
}
@@ -618,11 +634,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
618
634
e . preventDefault ( ) ;
619
635
} ;
620
636
621
- const handleOnMouseDownEvent = ( e : SyntheticEvent < Element > ) : void => {
622
- suppressEvent ( e ) ;
623
- focusInput ( ) ;
624
- } ;
625
-
626
637
const handleOnControlMouseDown = ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
627
638
if ( isDisabled ) return ;
628
639
if ( ! isFocused ) focusInput ( ) ;
@@ -657,20 +668,25 @@ const Select = forwardRef<SelectRef, SelectProps>((
657
668
setMenuOpen ( true ) ;
658
669
} , [ onInputChange ] ) ;
659
670
660
- const handleOnCaretMouseDown = useCallback ( ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
661
- handleOnMouseDownEvent ( e ) ;
662
- menuOpenRef . current ? setMenuOpen ( false ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
663
- } , [ openMenuAndFocusOption ] ) ;
671
+ const handleOnMouseDown = ( e : SyntheticEvent < Element > ) : void => {
672
+ suppressEvent ( e ) ;
673
+ focusInput ( ) ;
674
+ } ;
664
675
665
676
const handleOnClearMouseDown = useCallback ( ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
666
- handleOnMouseDownEvent ( e ) ;
677
+ handleOnMouseDown ( e ) ;
667
678
setSelectedOption ( EMPTY_ARRAY ) ;
668
679
} , [ ] ) ;
669
680
670
- const renderMenu = ! lazyLoadMenu || ( lazyLoadMenu && menuOpen ) ;
671
- const showClear = ! ! ( isClearable && ! isDisabled && hasSelectedOptions ) ;
681
+ const handleOnCaretMouseDown = useCallback ( ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
682
+ if ( ! isDisabled && ! openMenuOnClick ) {
683
+ handleOnMouseDown ( e ) ;
684
+ menuOpenRef . current ? setMenuOpen ( false ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
685
+ }
686
+ } , [ isDisabled , openMenuOnClick , openMenuAndFocusOption ] ) ;
687
+
688
+ const showClear = ! ! isClearable && ! isDisabled && hasSelectedOptions ;
672
689
const inputReadOnly = isDisabled || ! isSearchable || ! ! focusedMultiValue ;
673
- const handleOnCaretMouseDownOrNoop = ( ! isDisabled && ! openMenuOnClick ) ? handleOnCaretMouseDown : undefined ;
674
690
675
691
return (
676
692
< ThemeProvider theme = { theme } >
@@ -725,33 +741,32 @@ const Select = forwardRef<SelectRef, SelectProps>((
725
741
isDisabled = { isDisabled }
726
742
loadingNode = { loadingNode }
727
743
onClearMouseDown = { handleOnClearMouseDown }
728
- onCaretMouseDown = { handleOnCaretMouseDownOrNoop }
744
+ onCaretMouseDown = { handleOnCaretMouseDown }
729
745
/>
730
746
</ ControlWrapper >
731
- { renderMenu && (
732
- < Menu
733
- menuRef = { menuRef }
734
- menuOpen = { menuOpen }
735
- isLoading = { isLoading }
736
- menuTop = { menuStyleTop }
737
- height = { menuHeightCalc }
738
- itemSize = { menuItemSize }
739
- loadingMsg = { loadingMsg }
740
- menuOptions = { menuOptions }
741
- memoOptions = { memoOptions }
742
- fixedSizeListRef = { listRef }
743
- noOptionsMsg = { noOptionsMsg }
744
- selectOption = { selectOption }
745
- direction = { menuItemDirection }
746
- itemKeySelector = { itemKeySelector }
747
- overscanCount = { menuOverscanCount }
748
- menuPortalTarget = { menuPortalTarget }
749
- width = { menuWidth || theme . menu . width }
750
- renderOptionLabel = { renderOptionLabelFn }
751
- focusedOptionIndex = { focusedOption . index }
752
- onMenuMouseDown = { handleOnMouseDownEvent }
753
- />
754
- ) }
747
+ < Menu
748
+ menuRef = { menuRef }
749
+ menuOpen = { menuOpen }
750
+ isLoading = { isLoading }
751
+ menuTop = { menuStyleTop }
752
+ height = { menuHeightCalc }
753
+ itemSize = { menuItemSize }
754
+ loadingMsg = { loadingMsg }
755
+ menuOptions = { menuOptions }
756
+ memoOptions = { memoOptions }
757
+ fixedSizeListRef = { listRef }
758
+ lazyLoadMenu = { lazyLoadMenu }
759
+ noOptionsMsg = { noOptionsMsg }
760
+ selectOption = { selectOption }
761
+ direction = { menuItemDirection }
762
+ itemKeySelector = { itemKeySelector }
763
+ overscanCount = { menuOverscanCount }
764
+ menuPortalTarget = { menuPortalTarget }
765
+ onMenuMouseDown = { handleOnMouseDown }
766
+ width = { menuWidth || theme . menu . width }
767
+ renderOptionLabel = { renderOptionLabelFn }
768
+ focusedOptionIndex = { focusedOption . index }
769
+ />
755
770
{ isAriaLiveEnabled && (
756
771
< AriaLiveRegion
757
772
ariaLive = { ariaLive }
0 commit comments