feat(components): [autocomplete] add header and footer slot (#21594)

* feat(components): [autocomplete] add header and footer slot

* docs(components): add a bump version for new slots

* test(components): [autocomplete] add header/footer slots test case

* docs(components): [autocomplete] add header and footer slots example

---------

Co-authored-by: yinshenghao <shenghao.yin@eslink.com>
This commit is contained in:
catanswer
2025-08-06 04:34:39 +08:00
committed by GitHub
parent 41e65b79bb
commit 7b25a87d6d
5 changed files with 186 additions and 9 deletions

View File

@@ -47,6 +47,16 @@ autocomplete/custom-loading
:::
## Custom Header & Footer ^(2.11.0)
You can customize both the header and footer of the dropdown using slots
:::demo Use slot to customize the content.
autocomplete/custom-header-footer
:::
## API
### Attributes
@@ -87,14 +97,16 @@ autocomplete/custom-loading
### Slots
| Name | Description | Type |
| ---------------- | ------------------------------------ | ---------------------------------------- |
| default | custom content for input suggestions | ^[object]`{ item: Record<string, any> }` |
| prefix | content as Input prefix | - |
| suffix | content as Input suffix | - |
| prepend | content to prepend before Input | - |
| append | content to append after Input | - |
| loading ^(2.5.0) | override loading content | - |
| Name | Description | Type |
| ---------------- | ------------------------------------- | ---------------------------------------- |
| default | custom content for input suggestions | ^[object]`{ item: Record<string, any> }` |
| header ^(2.11.0) | content at the top of the dropdown | - |
| footer ^(2.11.0) | content at the bottom of the dropdown | - |
| prefix | content as Input prefix | - |
| suffix | content as Input suffix | - |
| prepend | content to prepend before Input | - |
| append | content to append after Input | - |
| loading ^(2.5.0) | override loading content | - |
### Exposes

View File

@@ -0,0 +1,106 @@
<template>
<div class="demo">
<div>
<p>Custom header content</p>
<el-autocomplete
v-model="headerSlotState"
:fetch-suggestions="querySearchAsync"
placeholder="Please input"
@select="handleSelect"
>
<template #header>header content</template>
</el-autocomplete>
</div>
<div>
<p>Custom footer content</p>
<el-autocomplete
ref="footerAutocompleteRef"
v-model="footerSlotstate"
:fetch-suggestions="querySearchAsync"
placeholder="Please input"
@select="handleSelect"
>
<template #footer>
<el-button link size="small" @click="handleClear"> Clear </el-button>
</template>
</el-autocomplete>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
const headerSlotState = ref('')
const footerSlotstate = ref('')
interface LinkItem {
value: string
link: string
}
const links = ref<LinkItem[]>([])
const loadAll = () => {
return [
{ value: 'vue', link: 'https://github.com/vuejs/vue' },
{ value: 'element', link: 'https://github.com/ElemeFE/element' },
{ value: 'cooking', link: 'https://github.com/ElemeFE/cooking' },
{ value: 'mint-ui', link: 'https://github.com/ElemeFE/mint-ui' },
{ value: 'vuex', link: 'https://github.com/vuejs/vuex' },
{ value: 'vue-router', link: 'https://github.com/vuejs/vue-router' },
{ value: 'babel', link: 'https://github.com/babel/babel' },
]
}
let timeout: ReturnType<typeof setTimeout>
const querySearchAsync = (queryString: string, cb: (arg: any) => void) => {
const results = queryString
? links.value.filter(createFilter(queryString))
: links.value
clearTimeout(timeout)
timeout = setTimeout(() => {
cb(results)
}, 3000 * Math.random())
}
const createFilter = (queryString: string) => {
return (restaurant: LinkItem) => {
return (
restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
)
}
}
const handleSelect = (item: Record<string, any>) => {
console.log(item)
}
onMounted(() => {
links.value = loadAll()
})
const footerAutocompleteRef = ref()
const handleClear = () => {
footerSlotstate.value = ''
footerAutocompleteRef.value.getData()
}
</script>
<style>
.demo {
display: flex;
}
.demo > div {
flex: 1;
text-align: center;
}
.demo > div > .el-autocomplete {
width: 50%;
}
.demo > div:not(:last-child) {
border-right: 1px solid var(--el-border-color);
}
</style>

View File

@@ -18,7 +18,8 @@ vi.useFakeTimers()
const _mount = (
payload = {},
type: 'fn-cb' | 'fn-promise' | 'fn-arr' | 'fn-async' | 'arr' = 'fn-cb',
defaultValue = ''
defaultValue = '',
slots: Record<string, any> = {}
) =>
mount(
defineComponent({
@@ -75,6 +76,7 @@ const _mount = (
v-model={state.value}
fetch-suggestions={querySearch}
{...state.payload}
v-slots={slots}
/>
)
},
@@ -482,4 +484,37 @@ describe('Autocomplete.vue', () => {
expect(container.attributes('aria-expanded')).toBe('true')
})
})
describe('new slots: header & footer', () => {
test('header slot renders', async () => {
const wrapper = _mount({ debounce: 0 }, 'fn-cb', '', {
header: () => 'Custom Header',
})
await wrapper.find('input').trigger('focus')
vi.runAllTimers()
await nextTick()
const headerEl = document.body.querySelector(
'.el-autocomplete-suggestion__header'
)
expect(headerEl).not.toBeNull()
expect(headerEl!.textContent).toBe('Custom Header')
})
test('should render footer slot', async () => {
const wrapper = _mount({ debounce: 0 }, 'fn-cb', '', {
footer: () => 'Custom Footer',
})
await nextTick()
await wrapper.find('input').trigger('focus')
vi.runAllTimers()
await nextTick()
const footerEl = document.body.querySelector(
'.el-autocomplete-suggestion__footer'
)
expect(footerEl).not.toBeNull()
expect(footerEl!.textContent).toBe('Custom Footer')
})
})
})

View File

@@ -68,6 +68,13 @@
}"
role="region"
>
<div
v-if="$slots.header"
:class="ns.be('suggestion', 'header')"
@click.stop
>
<slot name="header" />
</div>
<el-scrollbar
:id="listboxId"
tag="ul"
@@ -96,6 +103,13 @@
</li>
</template>
</el-scrollbar>
<div
v-if="$slots.footer"
:class="ns.be('suggestion', 'footer')"
@click.stop
>
<slot name="footer" />
</div>
</div>
</template>
</el-tooltip>

View File

@@ -26,6 +26,16 @@
border-radius: getCssVar('border-radius', 'base');
box-sizing: border-box;
@include e(header) {
padding: 10px;
border-bottom: 1px solid getCssVar('border-color', 'lighter');
}
@include e(footer) {
padding: 10px;
border-top: 1px solid getCssVar('border-color', 'lighter');
}
@include e(wrap) {
max-height: 280px;
padding: 10px 0;