feat(components): [select-v2] add fit-input-width prop (#18834)

* feat(components): [select-v2] add `width` prop

* feat(components): [select-v2] change `width` prop to `fit-input-width`

* docs(components): add description

* test(components): [select-v2] fix errors caused by canvas

* chore(components): optimize code and change documents

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

* feat: [select-v2] listen for `fit-input-width`

* fix: change the default value of the `fit-input-width` prop

* fix: the width did not change when remote search or creating temporary option

* docs: change document and example code

---------

Co-authored-by: thinkasany <480968828@qq.com>
Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
伊墨
2024-12-24 16:39:43 +08:00
committed by GitHub
parent 456cccdace
commit 2c9d70dc94
6 changed files with 120 additions and 4 deletions

View File

@@ -213,6 +213,16 @@ select-v2/custom-label
:::
## Custom Width ^(2.9.2)
The width of dropdown box is calculated by default based on the value of `label`. If you customize the dropdown box options through the `default slot`, it is likely that the text displayed in the options is not equal to the value of `label`, resulting in calculation errors. In this case, you can set the `fit-input-width` attribute to a number to fix its width.
:::demo
select-v2/custom-width
:::
## API
### Attributes
@@ -248,6 +258,7 @@ select-v2/custom-label
| persistent | when select dropdown is inactive and `persistent` is `false`, select dropdown will be destroyed | ^[boolean] | true |
| popper-options | [popper.js](https://popper.js.org/docs/v2/) parameters | ^[object]refer to [popper.js](https://popper.js.org/docs/v2/) doc | {} |
| automatic-dropdown | for non-filterable Select, this prop decides if the option menu pops up when the input is focused | ^[boolean] | false |
| fit-input-width ^(2.9.2) | whether the width of the dropdown is the same as the input, if the value is `number`, then the width is fixed | ^[boolean] / ^[number] | true |
| height | The height of the dropdown panel, 34px for each item | ^[number] | 274 |
| item-height | The height of the dropdown item | ^[number] | 34 |
| scrollbar-always-on | Controls whether the scrollbar is always displayed | ^[boolean] | false |

View File

@@ -0,0 +1,43 @@
<template>
<div class="flex flex-wrap gap-4 items-center">
<el-select-v2
v-model="value"
:options="options"
placeholder="Please select"
style="width: 240px"
:fit-input-width="false"
/>
<el-select-v2
v-model="value"
:options="options"
placeholder="Please select"
style="width: 240px"
fit-input-width
/>
<el-select-v2
v-model="value"
:options="options"
placeholder="Please select"
style="width: 240px"
:fit-input-width="440"
>
<template #default="{ item }">
<span>{{ item.value + item.label }}</span>
</template>
</el-select-v2>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
const value = ref()
const options = Array.from({ length: 1000 }).map((_, idx) => ({
value: `Option ${idx + 1}`,
label: `${initials[idx % 10]}${idx}${'-'.repeat(Math.ceil(idx / 25))}`,
}))
</script>

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import { nextTick, ref } from 'vue'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { NOOP, hasClass } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { makeMountFunc } from '@element-plus/test-utils/make-mount'
@@ -195,6 +195,10 @@ const PLACEHOLDER_CLASS_NAME = 'el-select__placeholder'
const DEFAULT_PLACEHOLDER = 'Select'
describe('Select', () => {
beforeAll(() => {
HTMLCanvasElement.prototype.getContext = vi.fn(() => null)
})
afterEach(() => {
document.body.innerHTML = ''
})

View File

@@ -8,6 +8,7 @@ import {
buildProps,
definePropType,
iconPropType,
isBoolean,
isNumber,
} from '@element-plus/utils'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
@@ -180,7 +181,7 @@ export const SelectProps = buildProps({
type: String,
},
/**
* @description whether select dropdown is teleported to the body
* @description whether select dropdown is teleported, if `true` it will be teleported to where `append-to` sets
*/
teleported: useTooltipContentProps.teleported,
/**
@@ -285,6 +286,18 @@ export const SelectProps = buildProps({
* @description which element the select dropdown appends to
*/
appendTo: String,
/**
* @description if it is `true`, the width of the dropdown panel is the same as the input box.
* if it is `false`, the width is automatically calculated based on the value of `label`,
* or it can be set to a number to make it a fixed width
*/
fitInputWidth: {
type: [Boolean, Number],
default: true,
validator(val) {
return isBoolean(val) || isNumber(val)
},
},
...useEmptyValuesProps,
...useAriaProps(['ariaLabel']),
} as const)

View File

@@ -20,6 +20,7 @@ import {
escapeStringRegexp,
isArray,
isFunction,
isNumber,
isObject,
} from '@element-plus/utils'
import {
@@ -353,7 +354,40 @@ const useSelect: useSelectReturnType = (
)
const calculatePopperSize = () => {
popperSize.value = selectRef.value?.offsetWidth || 200
if (isNumber(props.fitInputWidth)) {
popperSize.value = props.fitInputWidth
return
}
const width = selectRef.value?.offsetWidth || 200
if (!props.fitInputWidth && allOptions.value.length > 0) {
nextTick(() => {
popperSize.value = Math.max(width, calculateLabelMaxWidth())
})
} else {
popperSize.value = width
}
}
// TODO Caching implementation
// 1. There is no need to calculate options that have already been calculated
// 2. Repeatedly expand and close when persistent is set to false, no need for repeated calculations
const calculateLabelMaxWidth = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const selector = nsSelect.be('dropdown', 'item')
const dom = menuRef.value?.listRef?.innerRef || document
const dropdownItemEl = dom.querySelector(`.${selector}`)
if (dropdownItemEl === null || ctx === null) return 0
const style = getComputedStyle(dropdownItemEl)
const padding =
Number.parseFloat(style.paddingLeft) +
Number.parseFloat(style.paddingRight)
ctx.font = style.font
const maxWidth = filteredOptions.value.reduce((max, option) => {
const metrics = ctx.measureText(getLabel(option))
return Math.max(metrics.width, max)
}, 0)
return maxWidth + padding
}
const getGapWidth = () => {
@@ -880,12 +914,22 @@ const useSelect: useSelectReturnType = (
calculatePopperSize()
}
watch(
() => props.fitInputWidth,
() => {
calculatePopperSize()
}
)
// in order to track these individually, we need to turn them into refs instead of watching the entire
// reactive object which could cause perf penalty when unnecessary field gets changed the watch method will
// be invoked.
watch(expanded, (val) => {
if (val) {
if (!props.persistent) {
calculatePopperSize()
}
handleQueryChange('')
} else {
states.inputValue = ''
@@ -937,6 +981,7 @@ const useSelect: useSelectReturnType = (
watch(
() => filteredOptions.value,
() => {
calculatePopperSize()
return menuRef.value && nextTick(menuRef.value.resetScrollTop)
}
)

View File

@@ -161,7 +161,7 @@ export const SelectProps = buildProps({
default: 1,
},
/**
* @description whether select dropdown is teleported to the body
* @description whether select dropdown is teleported, if `true` it will be teleported to where `append-to` sets
*/
teleported: useTooltipContentProps.teleported,
/**