feat(components): [select/select-v2/cascader] expose more tag options (#21157)

* feat(components): [select/select-v2] bind `data` for tag slot

* chore: remove console.log

* feat: expose `deleteTag` aswell

---------

Co-authored-by: warmthsea <2586244885@qq.com>
This commit is contained in:
Noblet Ouways
2025-06-29 15:17:20 +02:00
committed by GitHub
parent 524f901045
commit 4f8cca0f6c
11 changed files with 241 additions and 34 deletions

View File

@@ -197,13 +197,13 @@ cascader/custom-tag
### Cascader Slots
| Name | Description | Scope |
| ------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------- |
| default | the custom content of cascader node, which are current Node object and node data respectively. | ^[object]`{ node: any, data: any }` |
| empty | content when there is no matched options. | — |
| prefix ^(2.9.4) | content as Input prefix | — |
| suggestion-item ^(2.9.5) | custom content for suggestion item when searching | ^[object]`{ item: CascaderNode }` |
| tag ^(2.10.3) | custom tags style | ^[object]`{ data: Tag[] }` |
| Name | Description | Scope |
| ------------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| default | the custom content of cascader node, which are current Node object and node data respectively. | ^[object]`{ node: any, data: any }` |
| empty | content when there is no matched options. | — |
| prefix ^(2.9.4) | content as Input prefix | — |
| suggestion-item ^(2.9.5) | custom content for suggestion item when searching | ^[object]`{ item: CascaderNode }` |
| tag ^(2.10.3) | custom tags style | ^[object]`{ data: Tag[], deleteTag: (tag: Tag) => void }` |
### Cascader Exposes

View File

@@ -303,16 +303,16 @@ select-v2/custom-width
### Slots
| Name | Description | Subtags |
| ---------------- | ----------------------------------------------------------------- | -------------------------------------- |
| default | Option renderer | — |
| header ^(2.5.2) | content at the top of the dropdown | — |
| footer ^(2.5.2) | content at the bottom of the dropdown | — |
| empty | content when options is empty | — |
| prefix | prefix content of input | — |
| tag ^(2.5.0) | content as Select tag, selectDisabled tag introduced in ^(2.10.3) | ^[object]`{ selectDisabled: boolean }` |
| loading ^(2.5.2) | content as Select loading | — |
| label ^(2.7.4) | content as Select label | — |
| Name | Description | Subtags |
| ---------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| default | Option renderer | — |
| header ^(2.5.2) | content at the top of the dropdown | — |
| footer ^(2.5.2) | content at the bottom of the dropdown | — |
| empty | content when options is empty | — |
| prefix | prefix content of input | — |
| tag ^(2.5.0) | content as Select tag, subTags `data`, `selectDisabled` and `deleteTag` introduced in ^(2.10.3) | ^[object]`{ data: Option[], selectDisabled: boolean, deleteTag: (event: MouseEvent, option: Option) => void }` |
| loading ^(2.5.2) | content as Select loading | — |
| label ^(2.7.4) | content as Select label | — |
### Exposes

View File

@@ -255,16 +255,16 @@ select/custom-label
### Select Slots
| Name | Description | Subtags |
| ---------------- | ----------------------------------------------------------------- | -------------------------------------- |
| default | option component list | Option Group / Option |
| header ^(2.4.3) | content at the top of the dropdown | — |
| footer ^(2.4.3) | content at the bottom of the dropdown | — |
| prefix | content as Select prefix | — |
| empty | content when there is no options | — |
| tag ^(2.5.0) | content as Select tag, selectDisabled tag introduced in ^(2.10.3) | ^[object]`{ selectDisabled: boolean }` |
| loading ^(2.5.2) | content as Select loading | — |
| label ^(2.7.4) | content as Select label | — |
| Name | Description | Subtags |
| ---------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| default | option component list | Option Group / Option |
| header ^(2.4.3) | content at the top of the dropdown | — |
| footer ^(2.4.3) | content at the bottom of the dropdown | — |
| prefix | content as Select prefix | — |
| empty | content when there is no options | — |
| tag ^(2.5.0) | content as Select tag, subTags `data`, `selectDisabled` and `deleteTag` introduced in ^(2.10.3) | ^[object]`{ data: OptionBasic[], selectDisabled: boolean, deleteTag: (event: MouseEvent, tag: OptionPublicInstance \| OptionBasic) => void }` |
| loading ^(2.5.2) | content as Select loading | — |
| label ^(2.7.4) | content as Select label | — |
### Select Exposes

View File

@@ -421,6 +421,52 @@ describe('Cascader.vue', () => {
expect(wrapper.find('.el-tag').classes()).toContain('el-tag--dark')
})
test('should expose delete-tag through slot & be able to delete a value', async () => {
const value = ref(['hangzhou', 'wenzhou'])
const wrapper = _mount(() => (
<Cascader
v-model={value.value}
options={[
{
value: 'zhejiang',
label: 'Zhejiang',
},
{
value: 'hangzhou',
label: 'Hangzhou',
},
{
value: 'ningbo',
label: 'Ningbo',
},
{
value: 'wenzhou',
label: 'Wenzhou',
},
]}
props={{ multiple: true }}
>
{{
tag: ({ data, deleteTag }: any) =>
data.map((option: any) => (
<div class="no-tag" onClick={() => deleteTag(option)}>
{option.text}
</div>
)),
}}
</Cascader>
))
await nextTick()
const cascader = wrapper.findComponent(Cascader)
const tags = wrapper.findAll('.no-tag')
expect(tags).toHaveLength(2)
expect(cascader.vm.modelValue).toEqual(['hangzhou', 'wenzhou'])
await tags[0].trigger('click')
expect(wrapper.findAll('.no-tag')).toHaveLength(1)
expect(cascader.vm.modelValue).toEqual([['wenzhou']])
})
test('filterable', async () => {
const value = ref([])
const wrapper = _mount(() => (

View File

@@ -73,7 +73,7 @@
nsCascader.is('validate', Boolean(validateState)),
]"
>
<slot name="tag" :data="allPresentTags">
<slot name="tag" :data="allPresentTags" :delete-tag="deleteTag">
<el-tag
v-for="tag in presentTags"
:key="tag.key"

View File

@@ -87,6 +87,7 @@ const createSelect = (
slots?: {
empty?: string
default?: string
tag?: string
}
} = {}
) => {
@@ -100,6 +101,12 @@ const createSelect = (
options.slots.default &&
`<template #default="{item}">${options.slots.default}</template>`) ||
''
const tagSlot =
(options.slots &&
options.slots.tag &&
`<template #tag="{ data, deleteTag }">${options.slots.tag}</template>`) ||
''
return _mount(
`
<el-select
@@ -140,6 +147,7 @@ const createSelect = (
v-model="value">
${defaultSlot}
${emptySlot}
${tagSlot}
</el-select>
`,
{
@@ -2148,6 +2156,73 @@ describe('Select', () => {
expect(wrapper.findAll('.el-tag').length).toBe(1)
})
it('should return slot tag data correctly & dont have tag component', async () => {
const value = [1, 3, 5]
const wrapper = createSelect({
data() {
return {
multiple: true,
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
{ label: 'Test 5', value: 5 },
],
value,
}
},
slots: {
tag: `
<span v-for="option in data" class="no-tag" :key="option.value">
{{ option.value }} - {{ option.label }}
</span>
`,
},
})
await nextTick()
const slotTagEls = wrapper.findAll('.no-tag')
expect(slotTagEls).toHaveLength(3)
expect(wrapper.find('.el-tag').exists()).toBe(false)
slotTagEls.forEach((el, idx) => {
expect(el.text()).toBe(`${value[idx]} - Test ${value[idx]}`)
})
})
it('should expose delete-tag through slot & be able to delete a value', async () => {
const wrapper = createSelect({
data() {
return {
multiple: true,
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
{ label: 'Test 5', value: 5 },
],
value: [2, 3, 5],
}
},
slots: {
tag: `
<span v-for="option in data" class="no-tag" :key="option.value" @click="deleteTag($event, option)">
{{ option.value }} - {{ option.label }}
</span>
`,
},
})
await nextTick()
const slotTagEls = wrapper.findAll('.no-tag')
expect(slotTagEls).toHaveLength(3)
expect(wrapper.vm.value).toEqual([2, 3, 5])
await slotTagEls[0].trigger('click')
expect(wrapper.findAll('.no-tag')).toHaveLength(2)
expect(wrapper.vm.value).toEqual([3, 5])
})
it('should be trigger the click event', async () => {
const handleClick = vi.fn()
const wrapper = _mount(`<el-select :options="[]" @click="handleClick" />`, {

View File

@@ -56,7 +56,13 @@
),
]"
>
<slot v-if="multiple" name="tag" :select-disabled="selectDisabled">
<slot
v-if="multiple"
name="tag"
:data="states.cachedOptions"
:delete-tag="deleteTag"
:select-disabled="selectDisabled"
>
<div
v-for="item in showTagList"
:key="getValueKey(getValue(item))"

View File

@@ -2902,6 +2902,75 @@ describe('Select', () => {
await nextTick()
expect(wrapper.findAll('.el-tag').length).toBe(1)
})
it('should return slot tag data correctly & dont have tag component', async () => {
const value = [1, 3, 5]
const wrapper = _mount(
`
<el-select v-model="value" multiple>
<el-option v-for="option in options" :value="option.value" :label="option.label"></el-option>
<template #tag="{ data }">
<span v-for="option in data" class="no-tag" :key="option.value">
{{ option.value }} - {{ option.currentLabel }}
</span>
</template>
</el-select>
`,
() => ({
value,
multiple: true,
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
{ label: 'Test 5', value: 5 },
],
})
)
await nextTick()
const slotTagEls = wrapper.findAll('.no-tag')
expect(slotTagEls).toHaveLength(3)
expect(wrapper.find('.el-tag').exists()).toBe(false)
slotTagEls.forEach((el, idx) => {
expect(el.text()).toBe(`${value[idx]} - Test ${value[idx]}`)
})
})
it('should expose delete-tag through slot & be able to delete a value', async () => {
const wrapper = _mount(
`
<el-select v-model="value" multiple>
<el-option v-for="option in options" :value="option.value" :label="option.label"></el-option>
<template #tag="{ data, deleteTag }">
<span v-for="option in data" class="no-tag" :key="option.value" @click="deleteTag($event, option)">
{{ option.value }} - {{ option.currentLabel }}
</span>
</template>
</el-select>
`,
() => ({
value: [2, 3, 5],
multiple: true,
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
{ label: 'Test 5', value: 5 },
],
})
)
await nextTick()
const slotTagEls = wrapper.findAll('.no-tag')
expect(slotTagEls).toHaveLength(3)
expect(wrapper.vm.value).toEqual([2, 3, 5])
await slotTagEls[0].trigger('click')
expect(wrapper.findAll('.no-tag')).toHaveLength(2)
expect(wrapper.vm.value).toEqual([3, 5])
})
it('It should generate accessible attributes', async () => {
wrapper = _mount(
`<el-select v-model="value">

View File

@@ -56,7 +56,13 @@
),
]"
>
<slot v-if="multiple" name="tag" :select-disabled="selectDisabled">
<slot
v-if="multiple"
name="tag"
:data="states.selected"
:delete-tag="deleteTag"
:select-disabled="selectDisabled"
>
<div
v-for="item in showTagList"
:key="getValueKey(item)"

View File

@@ -51,7 +51,12 @@ import {
import type { TooltipInstance } from '@element-plus/components/tooltip'
import type { ScrollbarInstance } from '@element-plus/components/scrollbar'
import type { SelectEmits, SelectProps } from './select'
import type { OptionPublicInstance, OptionValue, SelectStates } from './type'
import type {
OptionBasic,
OptionPublicInstance,
OptionValue,
SelectStates,
} from './type'
export const useSelect = (props: SelectProps, emit: SelectEmits) => {
const { t } = useLocale()
@@ -521,7 +526,7 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
const deleteTag = (
event: MouseEvent,
tag: OptionPublicInstance | SelectStates['selected'][0]
tag: OptionPublicInstance | OptionBasic
) => {
const index = states.selected.indexOf(tag)
if (index > -1 && !selectDisabled.value) {

View File

@@ -157,8 +157,8 @@
border-bottom: solid 1px getCssVar('border-color-lighter');
&.#{$namespace}-date-table__week-header {
padding: 0;
width: 24px;
padding: 0;
width: 24px;
}
}
}