@@ -2,6 +2,7 @@ import { useSyncRef } from '@react-aria/utils';
2
2
import { useDOMRef } from '@react-spectrum/utils' ;
3
3
import { DOMRef , FocusStrategy } from '@react-types/shared' ;
4
4
import React , {
5
+ Key ,
5
6
ReactElement ,
6
7
ReactNode ,
7
8
useCallback ,
@@ -133,7 +134,7 @@ function CommandMenuBase<T extends object>(
133
134
: undefined ;
134
135
135
136
const handleSelectionChange = onSelectionChange
136
- ? ( keys : any ) => {
137
+ ? ( keys : 'all' | Set < Key > ) => {
137
138
if ( keys === 'all' ) {
138
139
// Handle 'all' selection case - collect all available keys
139
140
const allKeys = Array . from ( treeState . collection . getKeys ( ) ) . map (
@@ -287,6 +288,7 @@ function CommandMenuBase<T extends object>(
287
288
288
289
// Track focused key for aria-activedescendant
289
290
const [ focusedKey , setFocusedKey ] = React . useState < React . Key | null > ( null ) ;
291
+ const focusedKeyRef = useRef < React . Key | null > ( null ) ;
290
292
291
293
// Apply filtering to collection items for rendering and empty state checks
292
294
const filteredCollectionItems = useMemo ( ( ) => {
@@ -482,28 +484,34 @@ function CommandMenuBase<T extends object>(
482
484
}
483
485
} , [ autoFocus , contextProps . autoFocus ] ) ;
484
486
487
+ // Track the previous search value to only run auto-focus when search actually changes
488
+ const prevSearchValueRef = useRef < string > ( '' ) ;
489
+
485
490
// Auto-focus first item when search value changes (but not on initial render)
486
491
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 !== '' ) {
489
497
const firstSelectableKey = findFirstSelectableItem ( ) ;
490
498
491
499
if ( firstSelectableKey && hasFilteredItems ) {
492
500
// Focus the first item in the selection manager
493
501
treeState . selectionManager . setFocusedKey ( firstSelectableKey ) ;
494
502
setFocusedKey ( firstSelectableKey ) ;
503
+ focusedKeyRef . current = firstSelectableKey ;
495
504
} else {
496
505
// Clear focus if no items are available
497
506
treeState . selectionManager . setFocusedKey ( null ) ;
498
507
setFocusedKey ( null ) ;
508
+ focusedKeyRef . current = null ;
499
509
}
500
510
}
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 ] ) ;
507
515
508
516
// Extract styles
509
517
const extractedStyles = useMemo (
@@ -580,50 +588,44 @@ function CommandMenuBase<T extends object>(
580
588
e . preventDefault ( ) ;
581
589
582
590
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
+ } ;
585
613
586
614
// Helper function to find next selectable key in a direction
587
615
const findNextSelectableKey = (
588
- startKey : any ,
616
+ currentIndex : number ,
589
617
direction : 'forward' | 'backward' ,
618
+ visibleKeys : Key [ ] ,
590
619
) => {
591
- if ( startKey == null ) {
592
- return null ;
593
- }
620
+ const increment = direction === 'forward' ? 1 : - 1 ;
594
621
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
601
626
) {
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 ) ) {
627
629
return key ;
628
630
}
629
631
}
@@ -634,50 +636,60 @@ function CommandMenuBase<T extends object>(
634
636
// Helper function to find first or last selectable key
635
637
const findFirstLastSelectableKey = (
636
638
direction : 'forward' | 'backward' ,
639
+ visibleKeys : Key [ ] ,
637
640
) => {
638
- const keys = [ ...collection . getKeys ( ) ] ;
639
641
const keysToCheck =
640
- direction === 'forward' ? keys : keys . reverse ( ) ;
642
+ direction === 'forward'
643
+ ? visibleKeys
644
+ : [ ...visibleKeys ] . reverse ( ) ;
641
645
642
646
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 ) ) {
650
648
return key ;
651
649
}
652
650
}
653
651
654
652
return null ;
655
653
} ;
656
654
655
+ const visibleKeys = getVisibleItemKeys ( ) ;
656
+
657
+ if ( visibleKeys . length === 0 ) {
658
+ return ; // No visible items to navigate
659
+ }
660
+
657
661
let nextKey ;
658
662
const direction = isArrowDown ? 'forward' : 'backward' ;
659
663
660
664
if ( currentKey == null ) {
661
665
// No current focus, start from the first/last item
662
- nextKey = findFirstLastSelectableKey ( direction ) ;
666
+ nextKey = findFirstLastSelectableKey ( direction , visibleKeys ) ;
663
667
} 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 ) ;
669
670
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
+ }
675
686
}
676
687
}
677
688
678
689
if ( nextKey != null ) {
679
690
selectionManager . setFocusedKey ( nextKey ) ;
680
691
setFocusedKey ( nextKey ) ;
692
+ focusedKeyRef . current = nextKey ; // Update ref immediately
681
693
}
682
694
} else if (
683
695
e . key === 'Enter' ||
0 commit comments