mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
106
docs/examples/autocomplete/custom-header-footer.vue
Normal file
106
docs/examples/autocomplete/custom-header-footer.vue
Normal 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>
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user