diff --git a/packages/react-aria/src/selection/useSelectableCollection.ts b/packages/react-aria/src/selection/useSelectableCollection.ts index 3b3bb8413db..99a04de7bd3 100644 --- a/packages/react-aria/src/selection/useSelectableCollection.ts +++ b/packages/react-aria/src/selection/useSelectableCollection.ts @@ -487,7 +487,11 @@ export function useSelectableCollection( ) { navigateToKey(manager.lastSelectedKey ?? delegate.getLastKey?.()); } else { - navigateToKey(manager.firstSelectedKey ?? delegate.getFirstKey?.()); + let firstKey = manager.firstSelectedKey ?? delegate.getFirstKey?.(); + if (firstKey != null && manager.disabledKeys.has(firstKey)) { + firstKey = delegate.getFirstKey?.() ?? null; + } + navigateToKey(firstKey); } } else if (scrollRef.current) { // Restore the scroll position to what it was before. @@ -597,7 +601,6 @@ export function useSelectableCollection( if (autoFocusRef.current) { let focusedKey: Key | null = null; - // Check focus strategy to determine which item to focus if (autoFocus === 'first') { focusedKey = delegate.getFirstKey?.() ?? null; } @@ -605,7 +608,6 @@ export function useSelectableCollection( focusedKey = delegate.getLastKey?.() ?? null; } - // If there are any selected keys, make the first one the new focus target let selectedKeys = manager.selectedKeys; if (selectedKeys.size) { for (let key of selectedKeys) { @@ -616,6 +618,36 @@ export function useSelectableCollection( } } + let originalStrategyKey = focusedKey; + let visitedKeys = new Set(); + let foundEnabledKey = false; + + while (focusedKey != null && !manager.canSelectItem(focusedKey)) { + if (visitedKeys.has(focusedKey)) { + break; + } + visitedKeys.add(focusedKey); + + let nextKey = delegate.getKeyBelow?.(focusedKey) ?? null; + + if (nextKey == null || nextKey === focusedKey) { + break; + } + + if (manager.canSelectItem(nextKey)) { + focusedKey = nextKey; + foundEnabledKey = true; + break; + } + + focusedKey = nextKey; + } + + // If no enabled key was found, restore original key (this may happen if all items are disabled) + if (!foundEnabledKey && focusedKey != null && !manager.canSelectItem(focusedKey)) { + focusedKey = originalStrategyKey; + } + manager.setFocused(true); manager.setFocusedKey(focusedKey); @@ -624,7 +656,6 @@ export function useSelectableCollection( focusSafely(ref.current); } - // Wait until the collection has items to autofocus. if (manager.collection.size > 0) { autoFocusRef.current = false; didAutoFocusRef.current = true; diff --git a/packages/react-aria/test/selection/useSelectableCollection.test.js b/packages/react-aria/test/selection/useSelectableCollection.test.js index b09e54d5c90..554fad452da 100644 --- a/packages/react-aria/test/selection/useSelectableCollection.test.js +++ b/packages/react-aria/test/selection/useSelectableCollection.test.js @@ -193,4 +193,21 @@ describe('useSelectableCollection', () => { expect(onSelectionChange2).not.toHaveBeenCalled(); }); }); + + describe('with disabled items boundary keys', () => { + it('should automatically focus the first available enabled item on autoFocus mount when leading items are disabled', () => { + let {getAllByRole} = render( + + Disabled Absolute First Item + First Enabled Item + Second Enabled Item + + ); + + let options = getAllByRole('option'); + + expect(document.activeElement).toBe(options[1]); + expect(options[1].textContent).toBe('First Enabled Item'); + }); + }); });