diff --git a/src/hooks/useCombobox/__tests__/getInputProps.test.js b/src/hooks/useCombobox/__tests__/getInputProps.test.js index 3358e45b..5812fb38 100644 --- a/src/hooks/useCombobox/__tests__/getInputProps.test.js +++ b/src/hooks/useCombobox/__tests__/getInputProps.test.js @@ -593,6 +593,21 @@ describe('getInputProps', () => { expect(getItems()).toHaveLength(items.length) }) + test('it skips disabled trailing options when opening the menu', async () => { + const {user} = renderCombobox({ + isItemDisabled(_item, index) { + return index >= items.length - 2 + }, + }) + + await keyDownOnInput(user, '{ArrowUp}') + + expect(getInput()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(items.length - 3), + ) + }) + test('it opens the closed menu with selected option highlighted', async () => { const selectedIndex = 4 const {user} = renderCombobox({ @@ -920,6 +935,21 @@ describe('getInputProps', () => { expect(getItems()).toHaveLength(items.length) }) + test('it skips disabled leading options when opening the menu', async () => { + const {user} = renderCombobox({ + isItemDisabled(_item, index) { + return index < 2 + }, + }) + + await keyDownOnInput(user, '{ArrowDown}') + + expect(getInput()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(2), + ) + }) + test('opens the closed menu with selected option highlighted', async () => { const selectedIndex = 4 const {user} = renderCombobox({ diff --git a/src/hooks/useSelect/__tests__/getToggleButtonProps.test.js b/src/hooks/useSelect/__tests__/getToggleButtonProps.test.js index 235f11ea..4ecc99c4 100644 --- a/src/hooks/useSelect/__tests__/getToggleButtonProps.test.js +++ b/src/hooks/useSelect/__tests__/getToggleButtonProps.test.js @@ -847,6 +847,21 @@ describe('getToggleButtonProps', () => { expect(getItems()).toHaveLength(items.length) }) + test('it skips disabled trailing options when opening the menu', async () => { + const {user} = renderSelect({ + isItemDisabled(_item, index) { + return index >= items.length - 2 + }, + }) + + await keyDownOnToggleButton(user, '{ArrowUp}') + + expect(getToggleButton()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(items.length - 3), + ) + }) + test('it opens the closed menu with selected option highlighted', async () => { const selectedIndex = 4 const {user} = renderSelect({ @@ -1188,6 +1203,21 @@ describe('getToggleButtonProps', () => { expect(getItems()).toHaveLength(items.length) }) + test('skips disabled leading options when opening the menu', async () => { + const {user} = renderSelect({ + isItemDisabled(_item, index) { + return index < 2 + }, + }) + + await keyDownOnToggleButton(user, '{ArrowDown}') + + expect(getToggleButton()).toHaveAttribute( + 'aria-activedescendant', + defaultIds.getItemId(2), + ) + }) + test('opens the closed menu with selected option highlighted', async () => { const selectedIndex = 4 const {user} = renderSelect({ diff --git a/src/hooks/utils/__tests__/getHighlightedIndexOnOpen.test.ts b/src/hooks/utils/__tests__/getHighlightedIndexOnOpen.test.ts index 664588f7..52cb1cb5 100644 --- a/src/hooks/utils/__tests__/getHighlightedIndexOnOpen.test.ts +++ b/src/hooks/utils/__tests__/getHighlightedIndexOnOpen.test.ts @@ -5,6 +5,9 @@ const isItemDisabled = () => false const isFirstItemDisabled = (_item: string, index: number) => index === 0 const isLastItemDisabled = (_item: string, index: number) => index === items.length - 1 +const areAllItemsDisabled = () => true +const areFirstTwoItemsDisabled = (_item: string, index: number) => index < 2 +const areLastTwoItemsDisabled = (_item: string, index: number) => index > 0 const itemToKey = (item: string | null) => item test('returns -1 when items is empty', () => { @@ -70,7 +73,7 @@ test('skips initialHighlightedIndex when state highlightedIndex does not match', ).toBe(-1) }) -test('skips initialHighlightedIndex when that item is disabled', () => { +test('falls back to the next enabled item when initialHighlightedIndex is disabled', () => { const initialHighlightedIndex = 0 const defaultHighlightedIndex = undefined const selectedItem = null @@ -88,7 +91,7 @@ test('skips initialHighlightedIndex when that item is disabled', () => { highlightedIndex, offset, ), - ).toBe(-1) + ).toBe(1) }) test('returns defaultHighlightedIndex when it is not disabled', () => { @@ -112,7 +115,7 @@ test('returns defaultHighlightedIndex when it is not disabled', () => { ).toBe(2) }) -test('skips defaultHighlightedIndex when that item is disabled', () => { +test('falls back to the next enabled item when defaultHighlightedIndex is disabled', () => { const initialHighlightedIndex = undefined const defaultHighlightedIndex = 0 const selectedItem = null @@ -130,7 +133,7 @@ test('skips defaultHighlightedIndex when that item is disabled', () => { highlightedIndex, offset, ), - ).toBe(-1) + ).toBe(1) }) test('returns index of selectedItem when selectedItem is set', () => { @@ -175,7 +178,7 @@ test('returns last index when offset is negative and last item is not disabled', ).toBe(2) }) -test('skips last index when offset is negative and last item is disabled', () => { +test('returns previous enabled index when offset is negative and last item is disabled', () => { const initialHighlightedIndex = undefined const defaultHighlightedIndex = undefined const selectedItem = null @@ -193,7 +196,22 @@ test('skips last index when offset is negative and last item is disabled', () => highlightedIndex, offset, ), - ).toBe(-1) + ).toBe(1) +}) + +test('skips consecutive disabled items when opening with a negative offset', () => { + expect( + getHighlightedIndexOnOpen( + items, + undefined, + undefined, + areLastTwoItemsDisabled, + itemToKey, + null, + -1, + -1, + ), + ).toBe(0) }) test('returns 0 when offset is positive and first item is not disabled', () => { @@ -217,7 +235,7 @@ test('returns 0 when offset is positive and first item is not disabled', () => { ).toBe(0) }) -test('skips 0 when offset is positive and first item is disabled', () => { +test('returns next enabled index when offset is positive and first item is disabled', () => { const initialHighlightedIndex = undefined const defaultHighlightedIndex = undefined const selectedItem = null @@ -235,9 +253,42 @@ test('skips 0 when offset is positive and first item is disabled', () => { highlightedIndex, offset, ), - ).toBe(-1) + ).toBe(1) +}) + +test('skips consecutive disabled items when opening with a positive offset', () => { + expect( + getHighlightedIndexOnOpen( + items, + undefined, + undefined, + areFirstTwoItemsDisabled, + itemToKey, + null, + -1, + 1, + ), + ).toBe(2) }) +test.each([-1, 1])( + 'returns -1 when all items are disabled and offset is %i', + offset => { + expect( + getHighlightedIndexOnOpen( + items, + undefined, + undefined, + areAllItemsDisabled, + itemToKey, + null, + -1, + offset, + ), + ).toBe(-1) + }, +) + test('returns -1 when no conditions match', () => { const initialHighlightedIndex = undefined const defaultHighlightedIndex = undefined diff --git a/src/hooks/utils/getHighlightedIndexOnOpen.ts b/src/hooks/utils/getHighlightedIndexOnOpen.ts index 08a19616..45dfc3e7 100644 --- a/src/hooks/utils/getHighlightedIndexOnOpen.ts +++ b/src/hooks/utils/getHighlightedIndexOnOpen.ts @@ -1,3 +1,5 @@ +import {getNonDisabledIndex} from '../../utils/getNonDisabledIndex' + /* eslint-disable max-params */ /** * Returns the highlighted index when the menu is opened. @@ -58,16 +60,11 @@ export function getHighlightedIndexOnOpen( return items.findIndex(item => itemToKey(selectedItem) === itemToKey(item)) } - if ( - offset < 0 && - items[items.length - 1] && - !isItemDisabled(items[items.length - 1] as Item, items.length - 1) - ) { - return items.length - 1 - } + if (offset !== 0) { + const backwards = offset < 0 + const startIndex = backwards ? items.length - 1 : 0 - if (offset > 0 && items[0] && !isItemDisabled(items[0], 0)) { - return 0 + return getNonDisabledIndex(startIndex, backwards, items, isItemDisabled) } return -1