+
+
+
+
+
+
+
+ {{ item.value + item.label }}
+
+
+
+
+
+
diff --git a/packages/components/select-v2/__tests__/select.test.ts b/packages/components/select-v2/__tests__/select.test.ts
index 2a21bafb2f..f628576578 100644
--- a/packages/components/select-v2/__tests__/select.test.ts
+++ b/packages/components/select-v2/__tests__/select.test.ts
@@ -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 = ''
})
diff --git a/packages/components/select-v2/src/defaults.ts b/packages/components/select-v2/src/defaults.ts
index 4ae9cb901e..1085ae52e6 100644
--- a/packages/components/select-v2/src/defaults.ts
+++ b/packages/components/select-v2/src/defaults.ts
@@ -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)
diff --git a/packages/components/select-v2/src/useSelect.ts b/packages/components/select-v2/src/useSelect.ts
index dfe9463cdb..49161b3301 100644
--- a/packages/components/select-v2/src/useSelect.ts
+++ b/packages/components/select-v2/src/useSelect.ts
@@ -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)
}
)
diff --git a/packages/components/select/src/select.ts b/packages/components/select/src/select.ts
index 4e7393db23..6b53720b09 100644
--- a/packages/components/select/src/select.ts
+++ b/packages/components/select/src/select.ts
@@ -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,
/**