feat(components): [autocomplete] add loopNavigation attributes (#22281)

* fix(components): [autocomplete] allow cyclic keyboard navigation

* fix: update

* docs: update

Co-authored-by: btea <2356281422@qq.com>

---------

Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
Zhong
2025-09-23 17:27:24 +08:00
committed by GitHub
parent ec86b5d6ad
commit b197bbf82d
4 changed files with 71 additions and 3 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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<typeof autocompleteProps>
export type AutocompletePropsPublic = __ExtractPublicPropTypes<

View File

@@ -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')}`