diff --git a/docs/en-US/component/autocomplete.md b/docs/en-US/component/autocomplete.md index 8fd969b050..a9abecac28 100644 --- a/docs/en-US/component/autocomplete.md +++ b/docs/en-US/component/autocomplete.md @@ -83,6 +83,7 @@ autocomplete/custom-header-footer | highlight-first-item | whether to highlight first item in remote search suggestions by default | ^[boolean] | false | | fit-input-width | whether the width of the dropdown is the same as the input | ^[boolean] | false | | popper-append-to-body ^(deprecated) | whether to append the dropdown to body. If the positioning of the dropdown is wrong, you can try to set this prop to false | ^[boolean] | false | +| loop-navigation ^(2.11.4) | whether keyboard navigation loops from end to start | ^[boolean] | true | | [input props](./input.md#attributes) | — | — | — | ### Events diff --git a/packages/components/autocomplete/__tests__/autocomplete.test.tsx b/packages/components/autocomplete/__tests__/autocomplete.test.tsx index 3cc9d1fd42..1c969f6175 100644 --- a/packages/components/autocomplete/__tests__/autocomplete.test.tsx +++ b/packages/components/autocomplete/__tests__/autocomplete.test.tsx @@ -364,6 +364,63 @@ describe('Autocomplete.vue', () => { expect(document.body.querySelector('.highlighted')).toBeDefined() }) + test('keyboard navigation should loop when loopNavigation is true', async () => { + const wrapper = _mount({ debounce: 10, loopNavigation: true }, 'arr') + await nextTick() + + const target = wrapper.getComponent(Autocomplete).vm as InstanceType< + typeof Autocomplete + > + const input = wrapper.find('input') + + await input.trigger('focus') + vi.runAllTimers() + await nextTick() + + const length = target.suggestions.length + + for (let i = 0; i < length; i++) { + await input.trigger('keydown.down') + expect(target.highlightedIndex).toBe(i) + } + + await input.trigger('keydown.down') + expect(target.highlightedIndex).toBe(0) + + await input.trigger('keydown.up') + expect(target.highlightedIndex).toBe(length - 1) + }) + + test('keyboard navigation should not loop when loopNavigation is false', async () => { + const wrapper = _mount({ debounce: 10, loopNavigation: false }, 'arr') + await nextTick() + + const target = wrapper.getComponent(Autocomplete).vm as InstanceType< + typeof Autocomplete + > + const input = wrapper.find('input') + + await input.trigger('focus') + vi.runAllTimers() + await nextTick() + + const length = target.suggestions.length + + await input.trigger('keydown.down') + expect(target.highlightedIndex).toBe(0) + + await input.trigger('keydown.up') + expect(target.highlightedIndex).toBe(-1) + + for (let i = 0; i < length; i++) { + await input.trigger('keydown.down') + expect(target.highlightedIndex).toBe(i) + } + + await input.trigger('keydown.down') + expect(target.highlightedIndex).toBe(length - 1) + }) + test('fitInputWidth', async () => { const wrapper = _mount({ fitInputWidth: true, diff --git a/packages/components/autocomplete/src/autocomplete.ts b/packages/components/autocomplete/src/autocomplete.ts index 8756d3968d..664ccb06db 100644 --- a/packages/components/autocomplete/src/autocomplete.ts +++ b/packages/components/autocomplete/src/autocomplete.ts @@ -114,6 +114,13 @@ export const autocompleteProps = buildProps({ * @description whether the width of the dropdown is the same as the input */ fitInputWidth: Boolean, + /** + * @description whether keyboard navigation loops from end to start + */ + loopNavigation: { + type: Boolean, + default: true, + }, } as const) export type AutocompleteProps = ExtractPropTypes export type AutocompletePropsPublic = __ExtractPublicPropTypes< diff --git a/packages/components/autocomplete/src/autocomplete.vue b/packages/components/autocomplete/src/autocomplete.vue index c3e404d7cd..87548a58de 100644 --- a/packages/components/autocomplete/src/autocomplete.vue +++ b/packages/components/autocomplete/src/autocomplete.vue @@ -348,12 +348,15 @@ const highlight = (index: number) => { if (!suggestionVisible.value || loading.value) return if (index < 0) { - highlightedIndex.value = -1 - return + if (!props.loopNavigation) { + highlightedIndex.value = -1 + return + } + index = suggestions.value.length - 1 } if (index >= suggestions.value.length) { - index = suggestions.value.length - 1 + index = props.loopNavigation ? 0 : suggestions.value.length - 1 } const suggestion = regionRef.value!.querySelector( `.${ns.be('suggestion', 'wrap')}`