mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
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:
@@ -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 |
|
||||
|
||||
43
docs/examples/select-v2/custom-width.vue
Normal file
43
docs/examples/select-v2/custom-width.vue
Normal 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>
|
||||
@@ -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 = ''
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user