@@ -2,6 +2,7 @@ import { useSyncRef } from '@react-aria/utils';
22import { useDOMRef } from '@react-spectrum/utils' ;
33import { DOMRef , FocusStrategy } from '@react-types/shared' ;
44import React , {
5+ Key ,
56 ReactElement ,
67 ReactNode ,
78 useCallback ,
@@ -133,7 +134,7 @@ function CommandMenuBase<T extends object>(
133134 : undefined ;
134135
135136 const handleSelectionChange = onSelectionChange
136- ? ( keys : any ) => {
137+ ? ( keys : 'all' | Set < Key > ) => {
137138 if ( keys === 'all' ) {
138139 // Handle 'all' selection case - collect all available keys
139140 const allKeys = Array . from ( treeState . collection . getKeys ( ) ) . map (
@@ -287,6 +288,7 @@ function CommandMenuBase<T extends object>(
287288
288289 // Track focused key for aria-activedescendant
289290 const [ focusedKey , setFocusedKey ] = React . useState < React . Key | null > ( null ) ;
291+ const focusedKeyRef = useRef < React . Key | null > ( null ) ;
290292
291293 // Apply filtering to collection items for rendering and empty state checks
292294 const filteredCollectionItems = useMemo ( ( ) => {
@@ -482,28 +484,34 @@ function CommandMenuBase<T extends object>(
482484 }
483485 } , [ autoFocus , contextProps . autoFocus ] ) ;
484486
487+ // Track the previous search value to only run auto-focus when search actually changes
488+ const prevSearchValueRef = useRef < string > ( '' ) ;
489+
485490 // Auto-focus first item when search value changes (but not on initial render)
486491 React . useEffect ( ( ) => {
487- // Only auto-focus when search value changes, not on initial mount
488- if ( searchValue . trim ( ) !== '' ) {
492+ const currentSearchValue = searchValue . trim ( ) ;
493+ const prevSearchValue = prevSearchValueRef . current ;
494+
495+ // Only auto-focus when search value actually changes
496+ if ( currentSearchValue !== prevSearchValue && currentSearchValue !== '' ) {
489497 const firstSelectableKey = findFirstSelectableItem ( ) ;
490498
491499 if ( firstSelectableKey && hasFilteredItems ) {
492500 // Focus the first item in the selection manager
493501 treeState . selectionManager . setFocusedKey ( firstSelectableKey ) ;
494502 setFocusedKey ( firstSelectableKey ) ;
503+ focusedKeyRef . current = firstSelectableKey ;
495504 } else {
496505 // Clear focus if no items are available
497506 treeState . selectionManager . setFocusedKey ( null ) ;
498507 setFocusedKey ( null ) ;
508+ focusedKeyRef . current = null ;
499509 }
500510 }
501- } , [
502- searchValue ,
503- findFirstSelectableItem ,
504- hasFilteredItems ,
505- treeState . selectionManager ,
506- ] ) ;
511+
512+ // Update the previous search value
513+ prevSearchValueRef . current = currentSearchValue ;
514+ } , [ searchValue , findFirstSelectableItem , hasFilteredItems ] ) ;
507515
508516 // Extract styles
509517 const extractedStyles = useMemo (
@@ -580,50 +588,44 @@ function CommandMenuBase<T extends object>(
580588 e . preventDefault ( ) ;
581589
582590 const isArrowDown = e . key === 'ArrowDown' ;
583- const { selectionManager, collection } = treeState ;
584- const currentKey = selectionManager . focusedKey ;
591+ const { selectionManager } = treeState ;
592+ // Use the ref to get the current focused key synchronously
593+ const currentKey =
594+ focusedKeyRef . current || selectionManager . focusedKey ;
595+
596+ // Helper function to get all visible item keys by applying filter to tree state collection
597+ const getVisibleItemKeys = ( ) : Key [ ] => {
598+ const keys : Key [ ] = [ ] ;
599+ const term = searchValue . trim ( ) ;
600+
601+ // Use the tree state's collection and apply filter manually
602+ for ( const item of treeState . collection ) {
603+ if ( item . type === 'item' ) {
604+ const text = item . textValue ?? String ( item . rendered ?? '' ) ;
605+ if ( enhancedFilter ( text , term , item . props ) ) {
606+ keys . push ( item . key ) ;
607+ }
608+ }
609+ }
610+
611+ return keys ;
612+ } ;
585613
586614 // Helper function to find next selectable key in a direction
587615 const findNextSelectableKey = (
588- startKey : any ,
616+ currentIndex : number ,
589617 direction : 'forward' | 'backward' ,
618+ visibleKeys : Key [ ] ,
590619 ) => {
591- if ( startKey == null ) {
592- return null ;
593- }
620+ const increment = direction === 'forward' ? 1 : - 1 ;
594621
595- // First check if the startKey itself is selectable
596- const startNode = collection . getItem ( startKey ) ;
597- if (
598- startNode &&
599- startNode . type === 'item' &&
600- ! selectionManager . isDisabled ( startKey )
622+ for (
623+ let i = currentIndex + increment ;
624+ i >= 0 && i < visibleKeys . length ;
625+ i += increment
601626 ) {
602- return startKey ;
603- }
604-
605- // If startKey is not selectable, find the next selectable key
606- let keys = [ ...collection . getKeys ( ) ] ;
607-
608- if ( direction === 'backward' ) {
609- keys = keys . reverse ( ) ;
610- }
611-
612- let startIndex = keys . indexOf ( startKey ) ;
613-
614- if ( startIndex === - 1 ) {
615- return null ;
616- }
617-
618- for ( let i = startIndex + 1 ; i < keys . length ; i ++ ) {
619- const key = keys [ i ] ;
620- const node = collection . getItem ( key ) ;
621-
622- if (
623- node &&
624- node . type === 'item' &&
625- ! selectionManager . isDisabled ( key )
626- ) {
627+ const key = visibleKeys [ i ] ;
628+ if ( ! selectionManager . isDisabled ( key ) ) {
627629 return key ;
628630 }
629631 }
@@ -634,50 +636,60 @@ function CommandMenuBase<T extends object>(
634636 // Helper function to find first or last selectable key
635637 const findFirstLastSelectableKey = (
636638 direction : 'forward' | 'backward' ,
639+ visibleKeys : Key [ ] ,
637640 ) => {
638- const keys = [ ...collection . getKeys ( ) ] ;
639641 const keysToCheck =
640- direction === 'forward' ? keys : keys . reverse ( ) ;
642+ direction === 'forward'
643+ ? visibleKeys
644+ : [ ...visibleKeys ] . reverse ( ) ;
641645
642646 for ( const key of keysToCheck ) {
643- const node = collection . getItem ( key ) ;
644-
645- if (
646- node &&
647- node . type === 'item' &&
648- ! selectionManager . isDisabled ( key )
649- ) {
647+ if ( ! selectionManager . isDisabled ( key ) ) {
650648 return key ;
651649 }
652650 }
653651
654652 return null ;
655653 } ;
656654
655+ const visibleKeys = getVisibleItemKeys ( ) ;
656+
657+ if ( visibleKeys . length === 0 ) {
658+ return ; // No visible items to navigate
659+ }
660+
657661 let nextKey ;
658662 const direction = isArrowDown ? 'forward' : 'backward' ;
659663
660664 if ( currentKey == null ) {
661665 // No current focus, start from the first/last item
662- nextKey = findFirstLastSelectableKey ( direction ) ;
666+ nextKey = findFirstLastSelectableKey ( direction , visibleKeys ) ;
663667 } else {
664- // Find next selectable item from current position
665- const candidateKey =
666- direction === 'forward'
667- ? collection . getKeyAfter ( currentKey )
668- : collection . getKeyBefore ( currentKey ) ;
668+ // Find current position in visible keys
669+ const currentIndex = visibleKeys . indexOf ( currentKey ) ;
669670
670- nextKey = findNextSelectableKey ( candidateKey , direction ) ;
671-
672- // If no next key found and focus wrapping is enabled, wrap to first/last selectable item
673- if ( nextKey == null ) {
674- nextKey = findFirstLastSelectableKey ( direction ) ;
671+ if ( currentIndex === - 1 ) {
672+ // Current key not in visible items, start from beginning/end
673+ nextKey = findFirstLastSelectableKey ( direction , visibleKeys ) ;
674+ } else {
675+ // Find next selectable item from current position
676+ nextKey = findNextSelectableKey (
677+ currentIndex ,
678+ direction ,
679+ visibleKeys ,
680+ ) ;
681+
682+ // If no next key found, wrap to first/last selectable item
683+ if ( nextKey == null ) {
684+ nextKey = findFirstLastSelectableKey ( direction , visibleKeys ) ;
685+ }
675686 }
676687 }
677688
678689 if ( nextKey != null ) {
679690 selectionManager . setFocusedKey ( nextKey ) ;
680691 setFocusedKey ( nextKey ) ;
692+ focusedKeyRef . current = nextKey ; // Update ref immediately
681693 }
682694 } else if (
683695 e . key === 'Enter' ||
0 commit comments