feat(components): [tree-select] add instance type and improve test cleanup logic (#22499)

* feat(components): [tree-select] add instance

* chore: update

* chore: update test

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

* Update packages/components/tree-select/src/instance.ts

Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>

---------

Co-authored-by: btea <2356281422@qq.com>
Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>
This commit is contained in:
sea
2025-11-28 23:34:57 +08:00
committed by GitHub
parent 8d4d0514e7
commit adf4fbfb6b
6 changed files with 107 additions and 59 deletions

View File

@@ -85,33 +85,33 @@ slider/show-marks
### Attributes
| Name | Description | Type | Default |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| model-value / v-model | binding value | ^[number] / ^[object]`number[]` | 0 |
| min | minimum value | ^[number] | 0 |
| max | maximum value | ^[number] | 100 |
| disabled | whether Slider is disabled | ^[boolean] | false |
| step | step size | ^[number] | 1 |
| show-input | whether to display an input box, works when `range` is false | ^[boolean] | false |
| show-input-controls | whether to display control buttons when `show-input` is true | ^[boolean] | true |
| size | size of the slider wrapper, will not work in vertical mode | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default |
| input-size | size of the input box, when set `size`, the default is the value of `size` | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default |
| show-stops | whether to display breakpoints | ^[boolean] | false |
| show-tooltip | whether to display tooltip value | ^[boolean] | true |
| format-tooltip | format to display tooltip value | ^[Function]`(value: number) => number \| string` | — |
| range | whether to select a range | ^[boolean] | false |
| vertical | vertical mode | ^[boolean] | false |
| height | slider height, required in vertical mode | ^[string] | — |
| aria-label ^(a11y) ^(2.7.2) | native `aria-label` attribute | ^[string] | — |
| range-start-label | when `range` is true, screen reader label for the start of the range | ^[string] | — |
| range-end-label | when `range` is true, screen reader label for the end of the range | ^[string] | — |
| format-value-text | format to display the `aria-valuenow` attribute for screen readers | ^[Function]`(value: number) => string` | — |
| tooltip-class | custom class name for the tooltip | ^[string] | — |
| placement | position of Tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top |
| marks | marks, type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | ^[object]`SliderMarks` | — |
| validate-event | whether to trigger form validation | ^[boolean] | true |
| Name | Description | Type | Default |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| model-value / v-model | binding value | ^[number] / ^[object]`number[]` | 0 |
| min | minimum value | ^[number] | 0 |
| max | maximum value | ^[number] | 100 |
| disabled | whether Slider is disabled | ^[boolean] | false |
| step | step size | ^[number] | 1 |
| show-input | whether to display an input box, works when `range` is false | ^[boolean] | false |
| show-input-controls | whether to display control buttons when `show-input` is true | ^[boolean] | true |
| size | size of the slider wrapper, will not work in vertical mode | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default |
| input-size | size of the input box, when set `size`, the default is the value of `size` | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default |
| show-stops | whether to display breakpoints | ^[boolean] | false |
| show-tooltip | whether to display tooltip value | ^[boolean] | true |
| format-tooltip | format to display tooltip value | ^[Function]`(value: number) => number \| string` | — |
| range | whether to select a range | ^[boolean] | false |
| vertical | vertical mode | ^[boolean] | false |
| height | slider height, required in vertical mode | ^[string] | — |
| aria-label ^(a11y) ^(2.7.2) | native `aria-label` attribute | ^[string] | — |
| range-start-label | when `range` is true, screen reader label for the start of the range | ^[string] | — |
| range-end-label | when `range` is true, screen reader label for the end of the range | ^[string] | — |
| format-value-text | format to display the `aria-valuenow` attribute for screen readers | ^[Function]`(value: number) => string` | — |
| tooltip-class | custom class name for the tooltip | ^[string] | — |
| placement | position of Tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top |
| marks | marks, type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | ^[object]`SliderMarks` | — |
| validate-event | whether to trigger form validation | ^[boolean] | true |
| persistent ^(2.9.5) | when slider tooltip inactive and `persistent` is `false` , tooltip will be destroyed. `persistent` always be `false` when `show-tooltip ` is `false` | ^[boolean] | true |
| label ^(a11y) ^(deprecated) | native `aria-label` attribute | ^[string] | — |
| label ^(a11y) ^(deprecated) | native `aria-label` attribute | ^[string] | — |
### Events

View File

@@ -186,7 +186,7 @@ tooltip/append-to
| virtual-triggering | Indicates whether virtual triggering is enabled | ^[boolean] | — |
| virtual-ref | Indicates the reference element to which the tooltip is attached | ^[HTMLElement] | — |
| trigger-keys | When you click the mouse to focus on the trigger element, you can define a set of keyboard codes to control the display of tooltip through the keyboard, not valid in controlled mode | ^[Array] | ['Enter','Space'] |
| persistent | when tooltip inactive and `persistent` is `false` , tooltip will be destroyed | ^[boolean] | — |
| persistent | when tooltip inactive and `persistent` is `false` , tooltip will be destroyed | ^[boolean] | — |
| aria-label ^(a11y) | same as `aria-label` | ^[string] | — |
| focus-on-target ^(2.11.2) | when triggering tooltips through hover, whether to focus the trigger element, which improves accessibility | ^[boolean] | false |

View File

@@ -1,16 +1,53 @@
import { nextTick, reactive, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, test, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { CircleClose } from '@element-plus/icons-vue'
import TreeSelect from '../src/tree-select.vue'
import Tree from '@element-plus/components/tree/src/tree.vue'
import defineGetter from '@element-plus/test-utils/define-getter'
import { EVENT_CODE } from '@element-plus/constants'
import type { TreeSelectInstance } from '../src/instance'
import type { RenderFunction } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
import type ElSelect from '@element-plus/components/select'
import type ElTree from '@element-plus/components/tree'
// Keep track of all mounted wrappers for cleanup
const mountedWrappers: VueWrapper<any>[] = []
beforeEach(() => {
document.body.innerHTML = ''
})
afterEach(async () => {
mountedWrappers.forEach((wrapper) => {
if (wrapper && wrapper.exists()) {
wrapper.unmount()
}
})
mountedWrappers.length = 0
document.body.innerHTML = ''
vi.clearAllTimers()
const frameIds = new Set<number>()
const originalRequestAnimationFrame = global.requestAnimationFrame
global.requestAnimationFrame = function (cb) {
const id = originalRequestAnimationFrame((timestamp) => {
frameIds.delete(id)
cb(timestamp)
})
frameIds.add(id)
return id
}
const cancelAllAnimationFrames = () => {
frameIds.forEach((id) => global.cancelAnimationFrame(id))
frameIds.clear()
}
cancelAllAnimationFrames()
await new Promise((resolve) => setTimeout(resolve, 0))
})
const createComponent = ({
slots = {},
@@ -19,7 +56,7 @@ const createComponent = ({
slots?: Record<string, any>
props?: (typeof TreeSelect)['props']
} = {}) => {
const wrapperRef = ref<InstanceType<typeof TreeSelect>>()
const wrapperRef = ref<TreeSelectInstance>()
const defaultData = ref([
{
value: 1,
@@ -53,9 +90,7 @@ const createComponent = ({
<TreeSelect
{...bindProps}
onUpdate:modelValue={(val: string) => (bindProps.modelValue = val)}
ref={(val: InstanceType<typeof TreeSelect>) =>
(wrapperRef.value = val)
}
ref={(val: TreeSelectInstance) => (wrapperRef.value = val)}
v-slots={slots}
/>
)
@@ -66,17 +101,22 @@ const createComponent = ({
}
)
// Add wrapper to tracking array for cleanup
mountedWrappers.push(wrapper)
return {
wrapper,
getWrapperRef: () =>
new Promise<InstanceType<typeof TreeSelect>>((resolve) =>
nextTick(() => resolve(wrapperRef.value!))
new Promise<TreeSelectInstance>((resolve) =>
nextTick(() =>
resolve(wrapperRef.value! as unknown as TreeSelectInstance)
)
),
select: wrapper.findComponent({ name: 'ElSelect' }) as VueWrapper<
InstanceType<typeof ElSelect>
>,
select: wrapper.findComponent({
name: 'ElSelect',
}) as VueWrapper<TreeSelectInstance['selectRef']>,
tree: wrapper.findComponent({ name: 'ElTree' }) as VueWrapper<
InstanceType<typeof ElTree>
TreeSelectInstance['treeRef']
>,
}
}
@@ -160,12 +200,12 @@ describe('TreeSelect.vue', () => {
await nextTick()
expect(select.vm.modelValue).toBe(1)
expect(wrapperRef.getCheckedKeys()).toEqual([1])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1])
value.value = 11
await nextTick(nextTick)
expect(select.vm.modelValue).toBe(11)
expect(wrapperRef.getCheckedKeys()).toEqual([11])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11])
await tree
.findAll('.el-select-dropdown__item')
@@ -173,17 +213,17 @@ describe('TreeSelect.vue', () => {
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(111)
expect(wrapperRef.getCheckedKeys()).toEqual([111])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([111])
await tree.find('.el-tree-node__content').trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(1)
expect(wrapperRef.getCheckedKeys()).toEqual([1])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1])
await tree.findAll('.el-checkbox__original')[1].trigger('click')
await nextTick()
expect(select.vm.modelValue).toBe(11)
expect(wrapperRef.getCheckedKeys()).toEqual([11])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11])
})
test('disabled', async () => {
@@ -235,12 +275,12 @@ describe('TreeSelect.vue', () => {
await nextTick()
expect(select.vm.modelValue).toEqual([1])
expect(wrapperRef.getCheckedKeys()).toEqual([1])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1])
value.value = [11]
await nextTick(nextTick)
expect(select.vm.modelValue).toEqual([11])
expect(wrapperRef.getCheckedKeys()).toEqual([11])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11])
await tree
.findAll('.el-select-dropdown__item')
@@ -248,17 +288,17 @@ describe('TreeSelect.vue', () => {
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([11, 111])
expect(wrapperRef.getCheckedKeys()).toEqual([11, 111])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11, 111])
await tree.find('.el-tree-node__content').trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([1, 11, 111])
expect(wrapperRef.getCheckedKeys()).toEqual([1, 11, 111])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1, 11, 111])
await tree.findAll('.el-checkbox')[1].trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([1, 111])
expect(wrapperRef.getCheckedKeys()).toEqual([1, 111])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1, 111])
})
test('filter', async () => {
@@ -384,14 +424,14 @@ describe('TreeSelect.vue', () => {
await tree.findAll('.el-tree-node__content')[0].trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([])
expect(wrapperRef.getCheckedKeys()).toEqual([])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([])
await tree
.findAll('.el-tree-node__content .el-checkbox')[0]
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([1])
expect(wrapperRef.getCheckedKeys()).toEqual([1])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1])
})
test('check-strictly showCheckbox checkOnClickNode click node', async () => {
@@ -408,14 +448,14 @@ describe('TreeSelect.vue', () => {
await tree.findAll('.el-tree-node__content')[0].trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([1])
expect(wrapperRef.getCheckedKeys()).toEqual([1])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1])
await tree
.findAll('.el-tree-node__content .el-checkbox')[0]
.trigger('click')
await nextTick()
expect(select.vm.modelValue).toEqual([])
expect(wrapperRef.getCheckedKeys()).toEqual([])
expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([])
})
test('only show checkbox', async () => {
@@ -978,6 +1018,10 @@ describe('TreeSelect.vue', () => {
},
template: `<TreeSelect v-for="item in data" v-model="item.value" :data="options" @update:modelValue="item.handleModelValue" />`,
})
// Add to tracking for cleanup
mountedWrappers.push(wrapper)
const select = wrapper.findComponent({
name: 'ElSelect',
})

View File

@@ -7,3 +7,5 @@ export const ElTreeSelect: SFCWithInstall<typeof TreeSelect> =
withInstall(TreeSelect)
export default ElTreeSelect
export type { TreeSelectInstance } from './src/instance'

View File

@@ -0,0 +1,7 @@
import type { SelectInstance } from '@element-plus/components/select'
import type { TreeInstance } from '@element-plus/components/tree'
export type TreeSelectInstance = {
treeRef: TreeInstance
selectRef: SelectInstance
}

5
typings/env.d.ts vendored
View File

@@ -1,4 +1,3 @@
import type { vShow } from 'vue'
import type { INSTALLED_KEY } from '@element-plus/constants'
declare global {
@@ -25,10 +24,6 @@ declare module 'vue' {
export interface GlobalComponents {
Component: (props: { is: Component | string }) => void
}
export interface ComponentCustomProperties {
vShow: typeof vShow
}
}
export {}