@@ -18,7 +18,7 @@ import {
1818 FilterMatchEnum ,
1919 OptionIndexEnum ,
2020 MenuPositionEnum ,
21- FUNCTION_DEFAULTS ,
21+ FUNCTIONS ,
2222 EMPTY_ARRAY ,
2323 DEFAULT_THEME ,
2424 SELECT_WRAPPER_ATTRS ,
@@ -51,11 +51,12 @@ import type { FixedSizeList } from 'react-window';
5151import styled , { css , ThemeProvider , type DefaultTheme } from 'styled-components' ;
5252import { Menu , Value , AriaLiveRegion , AutosizeInput , IndicatorIcons } from './components' ;
5353import { 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' ;
5555
5656type SelectProps = Readonly < {
5757 async ?: boolean ;
5858 inputId ?: string ;
59+ pageSize : number ;
5960 selectId ?: string ;
6061 isMulti ?: boolean ;
6162 ariaLabel ?: string ;
@@ -217,6 +218,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
217218 hideSelectedOptions,
218219 getIsOptionDisabled,
219220 getFilterOptionString,
221+ pageSize = 5 ,
220222 isSearchable = true ,
221223 memoOptions = false ,
222224 lazyLoadMenu = false ,
@@ -258,8 +260,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
258260 } , [ themeConfig ] ) ;
259261
260262 // 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 ] ) ;
263265 const renderOptionLabelFn = useMemo < RenderLabelCallback > ( ( ) => renderOptionLabel || getOptionLabelFn , [ renderOptionLabel , getOptionLabelFn ] ) ;
264266
265267 // Custom hook abstraction that debounces search input value (opt-in)
@@ -319,10 +321,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
319321 const blurInput = ( ) : void => inputRef . current ?. blur ( ) ;
320322 const focusInput = ( ) : void => inputRef . current ?. focus ( ) ;
321323 const scrollToItemIndex = ( idx : number ) : void => listRef . current ?. scrollToItem ( idx ) ;
322-
323- // Local boolean flags based on component props
324324 const hasSelectedOptions = isArrayWithLength ( selectedOption ) ;
325- const blurInputOnSelectOrDefault = isBoolean ( blurInputOnSelect ) ? blurInputOnSelect : IS_TOUCH_DEVICE ;
326325
327326 const openMenuAndFocusOption = useCallback ( ( position : OptionIndexEnum ) : void => {
328327 if ( ! isArrayWithLength ( menuOptions ) ) {
@@ -360,13 +359,14 @@ const Select = forwardRef<SelectRef, SelectProps>((
360359 setSelectedOption ( ( prev ) => ! isMulti ? [ selectedOpt ] : [ ...prev , selectedOpt ] ) ;
361360 }
362361
363- if ( blurInputOnSelectOrDefault ) {
362+ const blurOrDefault = isBoolean ( blurInputOnSelect ) ? blurInputOnSelect : isTouchDevice ( ) ;
363+ if ( blurOrDefault ) {
364364 blurInput ( ) ;
365365 } else if ( closeMenuOnSelect ) {
366366 setInputValue ( '' ) ;
367367 setMenuOpen ( false ) ;
368368 }
369- } , [ isMulti , closeMenuOnSelect , removeSelectedOption , blurInputOnSelectOrDefault ] ) ;
369+ } , [ isMulti , closeMenuOnSelect , blurInputOnSelect , removeSelectedOption ] ) ;
370370
371371 /**
372372 * useImperativeHandle.
@@ -508,15 +508,30 @@ const Select = forwardRef<SelectRef, SelectProps>((
508508 const focusOptionOnArrowKey = ( direction : OptionIndexEnum ) : void => {
509509 if ( ! isArrayWithLength ( menuOptions ) ) return ;
510510
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+ }
517532
518533 scrollToItemIndex ( index ) ;
519- setFocusedMultiValue ( null ) ;
534+ focusedMultiValue && setFocusedMultiValue ( null ) ;
520535 setFocusedOption ( { index, ...menuOptions [ index ] } ) ;
521536 } ;
522537
@@ -531,25 +546,29 @@ const Select = forwardRef<SelectRef, SelectProps>((
531546
532547 switch ( key ) {
533548 case 'ArrowDown' : {
534- menuOpen
535- ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN )
536- : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
549+ menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . DOWN ) : openMenuAndFocusOption ( OptionIndexEnum . FIRST ) ;
537550 break ;
538551 }
539552 case 'ArrowUp' : {
540- menuOpen
541- ? focusOptionOnArrowKey ( OptionIndexEnum . UP )
542- : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
553+ menuOpen ? focusOptionOnArrowKey ( OptionIndexEnum . UP ) : openMenuAndFocusOption ( OptionIndexEnum . LAST ) ;
543554 break ;
544555 }
545556 case 'ArrowLeft' :
546557 case 'ArrowRight' : {
547- if ( ! isMulti || inputValue || renderMultiOptions ) {
548- return ;
549- }
558+ if ( ! isMulti || inputValue || renderMultiOptions ) return ;
550559 focusValueOnArrowKey ( key ) ;
551560 break ;
552561 }
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+ }
553572 // Handle spacebar keydown events
554573 case ' ' : {
555574 if ( inputValue ) return ;
@@ -565,9 +584,8 @@ const Select = forwardRef<SelectRef, SelectProps>((
565584 break ;
566585 }
567586 case 'Enter' : {
568- if ( menuOpen ) {
569- selectOptionFromFocused ( ) ;
570- }
587+ if ( ! menuOpen ) return ;
588+ selectOptionFromFocused ( ) ;
571589 break ;
572590 }
573591 case 'Escape' : {
@@ -578,9 +596,7 @@ const Select = forwardRef<SelectRef, SelectProps>((
578596 break ;
579597 }
580598 case 'Tab' : {
581- if ( shiftKey || ! menuOpen || ! tabSelectsOption || ! focusedOption . data ) {
582- return ;
583- }
599+ if ( shiftKey || ! menuOpen || ! tabSelectsOption || ! focusedOption . data ) return ;
584600 selectOptionFromFocused ( ) ;
585601 break ;
586602 }
@@ -618,11 +634,6 @@ const Select = forwardRef<SelectRef, SelectProps>((
618634 e . preventDefault ( ) ;
619635 } ;
620636
621- const handleOnMouseDownEvent = ( e : SyntheticEvent < Element > ) : void => {
622- suppressEvent ( e ) ;
623- focusInput ( ) ;
624- } ;
625-
626637 const handleOnControlMouseDown = ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
627638 if ( isDisabled ) return ;
628639 if ( ! isFocused ) focusInput ( ) ;
@@ -657,20 +668,25 @@ const Select = forwardRef<SelectRef, SelectProps>((
657668 setMenuOpen ( true ) ;
658669 } , [ onInputChange ] ) ;
659670
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+ } ;
664675
665676 const handleOnClearMouseDown = useCallback ( ( e : MouseOrTouchEvent < HTMLElement > ) : void => {
666- handleOnMouseDownEvent ( e ) ;
677+ handleOnMouseDown ( e ) ;
667678 setSelectedOption ( EMPTY_ARRAY ) ;
668679 } , [ ] ) ;
669680
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 ;
672689 const inputReadOnly = isDisabled || ! isSearchable || ! ! focusedMultiValue ;
673- const handleOnCaretMouseDownOrNoop = ( ! isDisabled && ! openMenuOnClick ) ? handleOnCaretMouseDown : undefined ;
674690
675691 return (
676692 < ThemeProvider theme = { theme } >
@@ -725,33 +741,32 @@ const Select = forwardRef<SelectRef, SelectProps>((
725741 isDisabled = { isDisabled }
726742 loadingNode = { loadingNode }
727743 onClearMouseDown = { handleOnClearMouseDown }
728- onCaretMouseDown = { handleOnCaretMouseDownOrNoop }
744+ onCaretMouseDown = { handleOnCaretMouseDown }
729745 />
730746 </ 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+ />
755770 { isAriaLiveEnabled && (
756771 < AriaLiveRegion
757772 ariaLive = { ariaLive }
0 commit comments