From 0c9e5739837cccf66bd7d5f3d1b1f894cb538fcb Mon Sep 17 00:00:00 2001 From: Rylan Date: Tue, 14 Oct 2025 22:19:14 +0800 Subject: [PATCH 01/20] feat(Select): optimize custom node rendering --- packages/components/_util/parseTNode.ts | 18 ++- .../components/select-input/useSingle.tsx | 96 +++++++++++----- .../select/_example/custom-options.tsx | 104 ++++++++++-------- packages/components/select/base/Option.tsx | 15 ++- .../components/select/base/OptionGroup.tsx | 1 - packages/components/select/base/Select.tsx | 14 ++- 6 files changed, 163 insertions(+), 85 deletions(-) diff --git a/packages/components/_util/parseTNode.ts b/packages/components/_util/parseTNode.ts index e8d828b4f6..9ac89356a8 100644 --- a/packages/components/_util/parseTNode.ts +++ b/packages/components/_util/parseTNode.ts @@ -1,7 +1,7 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React, { type ReactElement, type ReactNode } from 'react'; import { isFunction } from 'lodash-es'; import log from '@tdesign/common-js/log/index'; -import { TNode } from '../common'; +import type { TNode } from '../common'; // 解析 TNode 数据结构 export default function parseTNode( @@ -37,3 +37,17 @@ export function parseContentTNode(tnode: TNode, props: T) { return null; } } + +export function extractTextFromTNode(node: TNode): string { + if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') return String(node); + if (React.isValidElement(node)) { + const { children } = node.props || {}; + if (children) return extractTextFromTNode(children); + } + if (Array.isArray(node)) { + return node.map(extractTextFromTNode).join(''); + } + + // todo:兼容 ((props: T) => ReactNode) 函数类型 + return ''; +} diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 9e92324767..573a46f72b 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -1,7 +1,7 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; -import { isObject, pick } from 'lodash-es'; +import { pick } from 'lodash-es'; import useConfig from '../hooks/useConfig'; import useControlled from '../hooks/useControlled'; @@ -33,24 +33,16 @@ const COMMON_PROPERTIES = [ 'prefixIcon', ]; -const DEFAULT_KEYS: TdSelectInputProps['keys'] = { - label: 'label', - value: 'value', -}; - -function getInputValue(value: TdSelectInputProps['value'], keys: TdSelectInputProps['keys']) { - const iKeys = keys || DEFAULT_KEYS; - return isObject(value) ? value[iKeys.label] : value; -} - export default function useSingle(props: TdSelectInputProps) { - const { value, keys, loading } = props; + const { value, loading } = props; const { classPrefix } = useConfig(); + const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); + const inputRef = useRef(null); const blurTimeoutRef = useRef(null); - const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); + const [labelWidth, setLabelWidth] = useState(0); const commonInputProps: SelectInputCommonProperties = { ...pick(props, COMMON_PROPERTIES), @@ -69,13 +61,21 @@ export default function useSingle(props: TdSelectInputProps) { } }; + useEffect(() => { + const labelEl = inputRef.current?.currentElement.querySelector(`.${classPrefix}-input__prefix`); + if (labelEl) { + const prefixWidth = labelEl.getBoundingClientRect().width; + setLabelWidth(prefixWidth); + } + }, [props.label, classPrefix]); + const renderSelectSingle = ( popupVisible: boolean, onInnerBlur?: (context: { e: React.FocusEvent }) => void, ) => { - // 单选,值的呈现方式 - const singleValueDisplay: any = !props.multiple ? props.valueDisplay : null; - const displayedValue = popupVisible && props.allowInput ? inputValue : getInputValue(value, keys); + const singleValueDisplay = !props.multiple ? props.valueDisplay : null; + + const showPseudoPlaceholder = inputValue?.length < 1 && React.isValidElement(singleValueDisplay); const handleBlur = (value, ctx) => { if (blurTimeoutRef.current) { @@ -104,22 +104,62 @@ export default function useSingle(props: TdSelectInputProps) { // !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' }); }; + const displayedValue = () => { + if (popupVisible && inputValue) { + return inputValue; + } + if (popupVisible && singleValueDisplay && !React.isValidElement(singleValueDisplay)) { + return ''; + } + if (!popupVisible && singleValueDisplay && !React.isValidElement(singleValueDisplay)) { + return String(singleValueDisplay); + } + return inputValue; + }; + + const displayedPlaceholder = () => { + if (popupVisible && singleValueDisplay && !React.isValidElement(singleValueDisplay)) { + return String(singleValueDisplay); + } + if (showPseudoPlaceholder) return ''; + return props.placeholder; + }; + + const pseudoPlaceholder = showPseudoPlaceholder ? ( +
+ {singleValueDisplay} +
+ ) : null; + return ( + {pseudoPlaceholder} + {commonInputProps.suffix} + + } autoWidth={props.autoWidth} allowInput={props.allowInput} - placeholder={singleValueDisplay ? '' : props.placeholder} - value={singleValueDisplay ? ' ' : displayedValue} - label={ - (props.label || singleValueDisplay) && ( - <> - {props.label} - {singleValueDisplay as React.ReactNode} - - ) - } + label={props.label} + value={displayedValue()} + placeholder={displayedPlaceholder()} onChange={onInnerInputChange} onClear={onInnerClear} // [Important Info]: SelectInput.blur is not equal to Input, example: click popup panel @@ -130,7 +170,7 @@ export default function useSingle(props: TdSelectInputProps) { // onBlur need to triggered by input when popup panel is null or when popupVisible is forced to false onBlur={handleBlur} {...props.inputProps} - inputClass={classNames(props.inputProps?.className, { + inputClass={classNames(props.inputProps?.inputClass, { [`${classPrefix}-input--focused`]: popupVisible, [`${classPrefix}-is-focused`]: popupVisible, })} diff --git a/packages/components/select/_example/custom-options.tsx b/packages/components/select/_example/custom-options.tsx index fd36ce046f..4a17df8594 100644 --- a/packages/components/select/_example/custom-options.tsx +++ b/packages/components/select/_example/custom-options.tsx @@ -1,55 +1,73 @@ import React, { useState } from 'react'; - -import { Select } from 'tdesign-react'; +import { Select, Space } from 'tdesign-react'; const { Option } = Select; -const options = [ - { label: '用户一', value: '1', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户二', value: '2', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户三', value: '3', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户四', value: '4', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户五', value: '5', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户六', value: '6', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户七', value: '7', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户八', value: '8', description: '这是一段用户描述信息,可自定义内容' }, - { label: '用户九', value: '9', description: '这是一段用户描述信息,可自定义内容' }, -]; - -const avatarUrl = 'https://tdesign.gtimg.com/site/avatar.jpg'; - -export default function CustomOptions() { +const generateCustomContent = (index: number) => ( +
+ +
+
用户{index}
+
+ 这是一段用户描述信息,可自定义内容 +
+
+
+); + +const createOption = (index: number) => { + const label = `用户${index}`; + return { + label, + value: index.toString(), + description: '这是一段用户描述信息,可自定义内容', + }; +}; + +const options1 = Array.from({ length: 5 }, (_, index) => ({ + ...createOption(index + 1), +})); + +const options2 = Array.from({ length: 5 }, (_, index) => ({ + ...createOption(index + 1), + content: generateCustomContent(index + 1), +})); + +function CustomOptions() { const [value, setValue] = useState('1'); const onChange = (value: string) => { setValue(value); }; return ( - + + + 法一:使用插槽 + + + + 法二:使用 `content` 属性 + +
+
+
+ + + 子选项二 + + + ( + 2.2 + ) + +
+
+
@@ -33494,7 +33502,6 @@ exports[`csr snapshot test > csr test packages/components/config-provider/_examp
csr test packages/components/select/_example/creata exports[`csr snapshot test > csr test packages/components/select/_example/custom-options.tsx 1`] = `
- - + 法一:使用插槽 + +
+
+
- - - - +
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+ + 法二:使用 \`content\` 属性 + +
+
+
+
+
+
+ + + + + + +
+
+
+
@@ -86859,22 +86955,17 @@ exports[`csr snapshot test > csr test packages/components/select/_example/custom
-
- 选中选项一 -
csr test packages/components/select-input/_example/
-
- - - - - - - - tdesign-vue - -
+
+
+ + + + + + + + tdesign-vue + +
+
@@ -145052,7 +145146,6 @@ exports[`csr snapshot test > csr test packages/components/tree-select/_example/f
csr test packages/components/tree-select/_example/p
csr test packages/components/tree-select/_example/v
-
- 广州市(guangzhou) -
ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
请选择
请选择
请选择
"`; @@ -149584,7 +149671,7 @@ exports[`ssr snapshot test > ssr test packages/components/config-provider/_examp exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/input.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; +exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/pagination.tsx 1`] = `"
Total 36 items
please select
  • 1
  • 2
  • 3
  • 4
/ 4
"`; @@ -150124,7 +150211,7 @@ exports[`ssr snapshot test > ssr test packages/components/select/_example/collap exports[`ssr snapshot test > ssr test packages/components/select/_example/creatable.tsx 1`] = `"
请选择
"`; -exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-options.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-options.tsx 1`] = `"
法一:使用插槽
法二:使用 \`content\` 属性
"`; exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-selected.tsx 1`] = `"
请选择
"`; @@ -150176,7 +150263,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; @@ -150538,7 +150625,7 @@ exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/b exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/collapsed.tsx 1`] = `"
广州市
+1
广州市
更多...
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/filterable.tsx 1`] = `"
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/filterable.tsx 1`] = `"
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/lazy.tsx 1`] = `"
"`; @@ -150548,11 +150635,11 @@ exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/p exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefix.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefixsuffix.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefixsuffix.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/props.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuedisplay.tsx 1`] = `"
广州市(guangzhou)
广州市(guangzhou)
深圳市(shenzhen)
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuedisplay.tsx 1`] = `"
广州市(guangzhou)
深圳市(shenzhen)
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuetype.tsx 1`] = `"
广州市
深圳市
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index be8eaee006..c451bd7b91 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -210,7 +210,7 @@ exports[`ssr snapshot test > ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
请选择
请选择
请选择
"`; @@ -274,7 +274,7 @@ exports[`ssr snapshot test > ssr test packages/components/config-provider/_examp exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/input.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; +exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/pagination.tsx 1`] = `"
Total 36 items
please select
  • 1
  • 2
  • 3
  • 4
/ 4
"`; @@ -814,7 +814,7 @@ exports[`ssr snapshot test > ssr test packages/components/select/_example/collap exports[`ssr snapshot test > ssr test packages/components/select/_example/creatable.tsx 1`] = `"
请选择
"`; -exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-options.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-options.tsx 1`] = `"
法一:使用插槽
法二:使用 \`content\` 属性
"`; exports[`ssr snapshot test > ssr test packages/components/select/_example/custom-selected.tsx 1`] = `"
请选择
"`; @@ -866,7 +866,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; @@ -1228,7 +1228,7 @@ exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/b exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/collapsed.tsx 1`] = `"
广州市
+1
广州市
更多...
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/filterable.tsx 1`] = `"
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/filterable.tsx 1`] = `"
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/lazy.tsx 1`] = `"
"`; @@ -1238,11 +1238,11 @@ exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/p exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefix.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefixsuffix.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/prefixsuffix.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/props.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuedisplay.tsx 1`] = `"
广州市(guangzhou)
广州市(guangzhou)
深圳市(shenzhen)
"`; +exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuedisplay.tsx 1`] = `"
广州市(guangzhou)
深圳市(shenzhen)
"`; exports[`ssr snapshot test > ssr test packages/components/tree-select/_example/valuetype.tsx 1`] = `"
广州市
深圳市
"`; From 259ed005424214d98f1aa23c94612fb8b394d844 Mon Sep 17 00:00:00 2001 From: Rylan Date: Wed, 15 Oct 2025 15:53:45 +0800 Subject: [PATCH 04/20] chore(Option): update deps import orders --- packages/components/select/base/Option.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/select/base/Option.tsx b/packages/components/select/base/Option.tsx index bc1fc06868..0192e3af7f 100644 --- a/packages/components/select/base/Option.tsx +++ b/packages/components/select/base/Option.tsx @@ -1,6 +1,6 @@ +import React, { useEffect, useMemo } from 'react'; import classNames from 'classnames'; import { get, isNumber, isString } from 'lodash-es'; -import React, { useEffect, useMemo } from 'react'; import useConfig from '../../hooks/useConfig'; import useDomRefCallback from '../../hooks/useDomRefCallback'; From ba428856fee6f4a9dcf19bb56e281974feba4cbf Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 16 Oct 2025 15:17:43 +0800 Subject: [PATCH 05/20] fix(useSingle): improve label handling and typing state management --- .../components/select-input/useSingle.tsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 4c3555ec39..39523068e7 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { isObject, pick } from 'lodash-es'; @@ -45,10 +45,10 @@ function getOptionLabel(value: TdSelectInputProps['value'], keys: TdSelectInputP export default function useSingle(props: TdSelectInputProps) { const { value, loading } = props; - - const optionLabel = getOptionLabel(value, props.keys); - const singleValueDisplay = props.valueDisplay ?? optionLabel; - const showLabelNode = React.isValidElement(singleValueDisplay); + const commonInputProps: SelectInputCommonProperties = { + ...pick(props, COMMON_PROPERTIES), + suffixIcon: loading ? : props.suffixIcon, + }; const { classPrefix } = useConfig(); const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); @@ -56,12 +56,18 @@ export default function useSingle(props: TdSelectInputProps) { const inputRef = useRef(null); const blurTimeoutRef = useRef(null); + const [isTyping, setIsTyping] = useState(false); const [labelWidth, setLabelWidth] = useState(0); - const commonInputProps: SelectInputCommonProperties = { - ...pick(props, COMMON_PROPERTIES), - suffixIcon: loading ? : props.suffixIcon, - }; + const singleValueDisplay = useMemo( + () => props.valueDisplay ?? getOptionLabel(value, props.keys), + [value, props.valueDisplay, props.keys], + ); + + const showLabelNode = useMemo( + () => !isTyping && !inputValue && React.isValidElement(singleValueDisplay), + [isTyping, inputValue, singleValueDisplay], + ); const onInnerClear = (context: { e: React.MouseEvent }) => { context?.e?.stopPropagation(); @@ -156,7 +162,7 @@ export default function useSingle(props: TdSelectInputProps) { return ( { + setIsTyping(true); + props.inputProps?.onCompositionstart?.(v, ctx); + }} + onCompositionend={(v, ctx) => { + setIsTyping(false); + props.inputProps?.onCompositionend?.(v, ctx); + }} inputClass={classNames(props.inputProps?.inputClass, { [`${classPrefix}-input--focused`]: popupVisible, [`${classPrefix}-is-focused`]: popupVisible, From 5766fcedcaf86d049d62839e517be80e8733c484 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 16 Oct 2025 15:54:29 +0800 Subject: [PATCH 06/20] chore(useSingle): rename variables for clarity --- packages/components/select-input/useSingle.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 39523068e7..4d5f12b606 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -64,7 +64,7 @@ export default function useSingle(props: TdSelectInputProps) { [value, props.valueDisplay, props.keys], ); - const showLabelNode = useMemo( + const showCustomElement = useMemo( () => !isTyping && !inputValue && React.isValidElement(singleValueDisplay), [isTyping, inputValue, singleValueDisplay], ); @@ -124,24 +124,24 @@ export default function useSingle(props: TdSelectInputProps) { if (popupVisible && inputValue) { return inputValue; } - if (props.allowInput && popupVisible && !showLabelNode) { + if (props.allowInput && popupVisible && !showCustomElement) { return ''; } - if (!showLabelNode) { + if (!showCustomElement) { return singleValueDisplay; } return inputValue; }; const displayedPlaceholder = () => { - if (popupVisible && singleValueDisplay && !showLabelNode) { + if (popupVisible && singleValueDisplay && !showCustomElement) { return singleValueDisplay; } - if (showLabelNode) return ''; + if (showCustomElement) return ''; return props.placeholder; }; - const labelNode = showLabelNode ? ( + const labelNode = showCustomElement ? (
Date: Thu, 23 Oct 2025 20:42:33 +0800 Subject: [PATCH 07/20] fix(Cascader): improve title handling --- .../components/cascader/components/Item.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/components/cascader/components/Item.tsx b/packages/components/cascader/components/Item.tsx index d5f0553a75..4fa41e0fbd 100644 --- a/packages/components/cascader/components/Item.tsx +++ b/packages/components/cascader/components/Item.tsx @@ -1,19 +1,18 @@ import React, { forwardRef, useMemo } from 'react'; -import classNames from 'classnames'; import { ChevronRightIcon as TdChevronRightIcon } from 'tdesign-icons-react'; - +import classNames from 'classnames'; import { isFunction } from 'lodash-es'; -import TLoading from '../../loading'; -import Checkbox from '../../checkbox'; +import Checkbox from '../../checkbox'; +import TLoading from '../../loading'; +import useCommonClassName from '../../hooks/useCommonClassName'; import useConfig from '../../hooks/useConfig'; -import useGlobalIcon from '../../hooks/useGlobalIcon'; import useDomRefCallback from '../../hooks/useDomRefCallback'; -import useCommonClassName from '../../hooks/useCommonClassName'; - -import { getFullPathLabel } from '../core/helper'; +import useGlobalIcon from '../../hooks/useGlobalIcon'; import { getCascaderItemClass, getCascaderItemIconClass } from '../core/className'; -import { CascaderContextType, TreeNodeValue, TreeNode } from '../interface'; +import { getFullPathLabel } from '../core/helper'; + +import type { CascaderContextType, TreeNode, TreeNodeValue } from '../interface'; const Item = forwardRef( ( @@ -90,9 +89,14 @@ const Item = forwardRef( const RenderLabelContent = (node: TreeNode, cascaderContext: CascaderContextType) => { const label = RenderLabelInner(node, cascaderContext); + const getTitle = () => { + const title = cascaderContext.inputVal ? getFullPathLabel(node) : node.label; + return typeof title !== 'object' ? title : undefined; + }; + const labelCont = ( From 58394d5358762d1b12b6670f3a20691af9968b71 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 27 Nov 2025 22:06:09 +0800 Subject: [PATCH 08/20] fix: `autoWidth` --- .../components/select-input/useSingle.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 4d5f12b606..d0e487cde7 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -9,6 +9,7 @@ import Input, { type InputRef, type TdInputProps } from '../input'; import Loading from '../loading'; import type { SelectInputCommonProperties } from './interface'; +import type { SelectInputProps } from './SelectInput'; import type { TdSelectInputProps } from './type'; export interface RenderSelectSingleInputParams { @@ -43,7 +44,7 @@ function getOptionLabel(value: TdSelectInputProps['value'], keys: TdSelectInputP return isObject(value) ? value[iKeys.label] : value; } -export default function useSingle(props: TdSelectInputProps) { +export default function useSingle(props: SelectInputProps) { const { value, loading } = props; const commonInputProps: SelectInputCommonProperties = { ...pick(props, COMMON_PROPERTIES), @@ -55,9 +56,11 @@ export default function useSingle(props: TdSelectInputProps) { const inputRef = useRef(null); const blurTimeoutRef = useRef(null); + const customElementRef = useRef(null); const [isTyping, setIsTyping] = useState(false); const [labelWidth, setLabelWidth] = useState(0); + const [customElementWidth, setCustomElementWidth] = useState(0); const singleValueDisplay = useMemo( () => props.valueDisplay ?? getOptionLabel(value, props.keys), @@ -89,6 +92,13 @@ export default function useSingle(props: TdSelectInputProps) { } }, [props.label, classPrefix]); + useEffect(() => { + if (showCustomElement && customElementRef.current) { + const { width } = customElementRef.current.getBoundingClientRect(); + setCustomElementWidth(width); + } + }, [showCustomElement, singleValueDisplay]); + const renderSelectSingle = ( popupVisible: boolean, onInnerBlur?: (context: { e: React.FocusEvent }) => void, @@ -143,9 +153,10 @@ export default function useSingle(props: TdSelectInputProps) { const labelNode = showCustomElement ? (
) : null; + const hasCustomWidth = props.style?.width || props.inputProps?.style?.width || props.inputProps?.style?.minWidth; + // customElement 定位为 absolute,无法撑开 input 宽度 + const inputWidth = + !hasCustomWidth && showCustomElement && customElementWidth > 0 + ? `${customElementWidth + labelWidth + 48}px` + : undefined; + return ( Date: Fri, 19 Dec 2025 10:24:35 +0800 Subject: [PATCH 09/20] chore: update snapshots --- packages/common | 2 +- test/snap/__snapshots__/csr.test.jsx.snap | 6 +----- test/snap/__snapshots__/ssr.test.jsx.snap | 6 +----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/common b/packages/common index 7155e11c03..054ebf90bb 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 7155e11c0395c67bbef5f1687c8447f6265386a1 +Subproject commit 054ebf90bbed57efb922ff007471ba18cb53a2e8 diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 9674065abd..329b5960b4 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -150765,11 +150765,7 @@ exports[`ssr snapshot test > ssr test packages/components/config-provider/_examp exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/input.tsx 1`] = `"
"`; -<<<<<<< HEAD -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; -======= -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; ->>>>>>> origin +exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/pagination.tsx 1`] = `"
Total 36 items
please select
  • 1
  • 2
  • 3
  • 4
/ 4
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index afc6905f75..bde8cbcf7b 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -274,11 +274,7 @@ exports[`ssr snapshot test > ssr test packages/components/config-provider/_examp exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/input.tsx 1`] = `"
"`; -<<<<<<< HEAD -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; -======= -exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; ->>>>>>> origin +exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/others.tsx 1`] = `"
Feature Tag
Feature Tag
Feature Tag
Feature Tag
Tree Empty Data
First Step
You need to click the blue button
Second Step
Fill your base information into the form
Error Step
Something Wrong! Custom Error Icon!
4
Last Step
You haven't finish this step.
图片加载中
"`; exports[`ssr snapshot test > ssr test packages/components/config-provider/_example/pagination.tsx 1`] = `"
Total 36 items
please select
  • 1
  • 2
  • 3
  • 4
/ 4
"`; From 817211bdb163aa02e696fe52f62dcb5bcf94b934 Mon Sep 17 00:00:00 2001 From: Rylan Date: Tue, 12 May 2026 17:51:30 +0800 Subject: [PATCH 10/20] fix: calc width --- .../components/select-input/useSingle.tsx | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index d0e487cde7..05fad7d08a 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -56,11 +56,12 @@ export default function useSingle(props: SelectInputProps) { const inputRef = useRef(null); const blurTimeoutRef = useRef(null); - const customElementRef = useRef(null); + const customElementRef = useRef(null); const [isTyping, setIsTyping] = useState(false); const [labelWidth, setLabelWidth] = useState(0); const [customElementWidth, setCustomElementWidth] = useState(0); + const [suffixSpace, setSuffixSpace] = useState(0); const singleValueDisplay = useMemo( () => props.valueDisplay ?? getOptionLabel(value, props.keys), @@ -99,6 +100,45 @@ export default function useSingle(props: SelectInputProps) { } }, [showCustomElement, singleValueDisplay]); + useEffect(() => { + const inputEl = inputRef.current?.inputElement; + if (!inputEl || !props.autoWidth) return; + if (showCustomElement && customElementWidth > 0) { + inputEl.style.minWidth = `${customElementWidth}px`; + } else { + inputEl.style.minWidth = ''; + } + }, [props.autoWidth, showCustomElement, customElementWidth]); + + useEffect(() => { + // 自定义 valueDisplay 时,labelNode 使用绝对定位 + // 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白 + if (!showCustomElement) { + setSuffixSpace(0); + return; + } + const wrapperEl = inputRef.current?.currentElement; + const inputEl = inputRef.current?.inputElement; + if (!wrapperEl || !inputEl) return undefined; + + const measure = () => { + const wrapperRect = wrapperEl.getBoundingClientRect(); + const inputRect = inputEl.getBoundingClientRect(); + // wrapper 右内边距 + suffix 区域 + suffixIcon 区域 + const space = Math.max(wrapperRect.right - inputRect.right, 0); + setSuffixSpace(space); + }; + + measure(); + + wrapperEl.addEventListener('mouseenter', measure); + wrapperEl.addEventListener('mouseleave', measure); + return () => { + wrapperEl.removeEventListener('mouseenter', measure); + wrapperEl.removeEventListener('mouseleave', measure); + }; + }, [showCustomElement, singleValueDisplay, props.clearable, props.suffixIcon, props.suffix]); + const renderSelectSingle = ( popupVisible: boolean, onInnerBlur?: (context: { e: React.FocusEvent }) => void, @@ -130,7 +170,7 @@ export default function useSingle(props: SelectInputProps) { // !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' }); }; - const displayedValue = () => { + const displayedValue = (): string => { if (popupVisible && inputValue) { return inputValue; } @@ -143,7 +183,7 @@ export default function useSingle(props: SelectInputProps) { return inputValue; }; - const displayedPlaceholder = () => { + const displayedPlaceholder = (): string => { if (popupVisible && singleValueDisplay && !showCustomElement) { return singleValueDisplay; } @@ -153,20 +193,22 @@ export default function useSingle(props: SelectInputProps) { const labelNode = showCustomElement ? (
- {singleValueDisplay} + + {singleValueDisplay} +
) : null; From 7abbf3ab6e470550dd7ae936f1dd2f5dd815a7ed Mon Sep 17 00:00:00 2001 From: Rylan Date: Tue, 12 May 2026 18:19:07 +0800 Subject: [PATCH 11/20] chore: update snap --- test/snap/__snapshots__/csr.test.jsx.snap | 92 ++++++++++++----------- test/snap/__snapshots__/ssr.test.jsx.snap | 4 +- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index e2ed571c35..f7166d95e5 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -22917,22 +22917,26 @@ exports[`csr snapshot test > csr test packages/components/cascader/_example/valu class="t-input__suffix" >
-
- - - 子选项二 - - - ( - 2.2 - ) - -
+ +
+ + + 子选项二 + + + ( + 2.2 + ) + +
+
csr test packages/components/select-input/_example/ class="t-input__suffix" >
- - - - - - - tdesign-vue + + + + + + tdesign-vue +
@@ -153002,7 +153010,7 @@ exports[`ssr snapshot test > ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
onlyLeaf
请选择
parentFirst
请选择
all
请选择
"`; @@ -153662,7 +153670,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index 5c4fe66c2a..e1e50d529a 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -210,7 +210,7 @@ exports[`ssr snapshot test > ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
onlyLeaf
请选择
parentFirst
请选择
all
请选择
"`; @@ -870,7 +870,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; From a3a6c5550e495d0744317eb75e166bf705c7231b Mon Sep 17 00:00:00 2001 From: Rylan Date: Mon, 1 Jun 2026 17:22:20 +0800 Subject: [PATCH 12/20] refactor: use `min-height` instead of `height` for custom element --- packages/common | 2 +- packages/components/select/base/Option.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/common b/packages/common index 1c930580be..7249f43c1c 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 1c930580be92a98e431ec3ef76af0b7d98c529d6 +Subproject commit 7249f43c1c2c17f2e6dc715566c668491a8224c7 diff --git a/packages/components/select/base/Option.tsx b/packages/components/select/base/Option.tsx index 8c0c355e2b..cca5c263cf 100644 --- a/packages/components/select/base/Option.tsx +++ b/packages/components/select/base/Option.tsx @@ -71,7 +71,6 @@ const Option: React.FC = (props) => { const [allSelectableChecked, setAllSelectableChecked] = useState(initCheckedStatus); const displayedContent = children || content || label; - const isCustomElement = React.isValidElement(displayedContent); const titleContent = useMemo(() => { // 外部设置 props,说明希望受控 @@ -173,10 +172,7 @@ const Option: React.FC = (props) => { key={value} onClick={handleSelect} ref={setRefCurrent} - style={{ - ...(isCustomElement ? { height: 'auto' } : {}), - ...style, - }} + style={style} > {renderItem()} From 2b9024e9040fe25bc33cbcb4cb7b6c7367ff06a4 Mon Sep 17 00:00:00 2001 From: Rylan Date: Tue, 16 Jun 2026 16:41:09 +0800 Subject: [PATCH 13/20] fix: avoid `Cannot read properties of null` --- packages/components/input/Input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/input/Input.tsx b/packages/components/input/Input.tsx index e2c77d0f4d..da931b8c54 100644 --- a/packages/components/input/Input.tsx +++ b/packages/components/input/Input.tsx @@ -166,7 +166,7 @@ const Input = forwardRefWithStatics( ) : null; const updateInputWidth = () => { - if (!autoWidth || !inputRef.current) return; + if (!autoWidth || !inputRef.current || !inputPreRef.current) return; const { offsetWidth } = inputPreRef.current; const { width } = inputPreRef.current.getBoundingClientRect(); // 异步渲染场景下 getBoundingClientRect 宽度为 0,需要使用 offsetWidth From 7e9bc7551ab0e98e42aab5f4174858c6610f5cd0 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 25 Jun 2026 15:31:16 +0800 Subject: [PATCH 14/20] fix: auto width --- packages/components/select-input/useSingle.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 017e3cd0a2..fc9ca04571 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -45,7 +45,7 @@ function getOptionLabel(value: TdSelectInputProps['value'], keys: TdSelectInputP } export default function useSingle(props: SelectInputProps) { - const { value, loading } = props; + const { value, loading, autoWidth } = props; const commonInputProps: SelectInputCommonProperties = { ...pick(props, COMMON_PROPERTIES), suffixIcon: loading ? : props.suffixIcon, @@ -102,13 +102,13 @@ export default function useSingle(props: SelectInputProps) { useEffect(() => { const inputEl = inputRef.current?.inputElement; - if (!inputEl || !props.autoWidth) return; + if (!inputEl || !autoWidth) return; if (showCustomElement && customElementWidth > 0) { inputEl.style.minWidth = `${customElementWidth}px`; } else { inputEl.style.minWidth = ''; } - }, [props.autoWidth, showCustomElement, customElementWidth]); + }, [autoWidth, showCustomElement, customElementWidth]); useEffect(() => { // 自定义 valueDisplay 时,labelNode 使用绝对定位 @@ -206,7 +206,10 @@ export default function useSingle(props: SelectInputProps) { opacity: popupVisible && props.allowInput ? 0.5 : undefined, }} > - + {singleValueDisplay}
@@ -235,7 +238,7 @@ export default function useSingle(props: SelectInputProps) { )) } - autoWidth={props.autoWidth} + autoWidth={autoWidth} style={{ ...(props.inputProps?.style || {}), minWidth: inputWidth, From 77f56055ed055dabff881a2850bd06e86059b15f Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 25 Jun 2026 15:46:28 +0800 Subject: [PATCH 15/20] fix: revert lint --- .eslintignore | 1 + package.json | 2 +- packages/components/input-number/type.ts | 72 ++++++------------------ packages/components/tag-input/type.ts | 62 +++++--------------- 4 files changed, 34 insertions(+), 103 deletions(-) diff --git a/.eslintignore b/.eslintignore index f5a9cb43b9..5550551bd0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,3 +15,4 @@ static/ packages/common packages/ai-core site/engineering/static +packages/components/**/type.ts \ No newline at end of file diff --git a/package.json b/package.json index 41d335b72d..8f0d5921ec 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ } }, "lint-staged": { - "packages/**/*.{ts,tsx}": [ + "packages/**/!(type).{ts,tsx}": [ "prettier --write", "pnpm run lint:fix" ] diff --git a/packages/components/input-number/type.ts b/packages/components/input-number/type.ts index a0e407ba49..12cc949929 100644 --- a/packages/components/input-number/type.ts +++ b/packages/components/input-number/type.ts @@ -4,21 +4,15 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { InputProps } from "../input"; -import type { TNode } from "../common"; -import type { - MouseEvent, - KeyboardEvent, - FocusEvent, - FormEvent, - CompositionEvent, -} from "react"; +import { InputProps } from '../input'; +import type { TNode } from '../common'; +import type { MouseEvent, KeyboardEvent, FocusEvent, FormEvent, CompositionEvent } from 'react'; export interface TdInputNumberProps { /** * 文本内容位置,居左/居中/居右 */ - align?: "left" | "center" | "right"; + align?: 'left' | 'center' | 'right'; /** * 是否允许输入超过 `max` `min` 范围外的数字。为保障用户体验,仅在失去焦点时进行数字范围矫正。默认允许超出,数字超出范围时,输入框变红提醒 * @default true @@ -45,10 +39,7 @@ export interface TdInputNumberProps { /** * 格式化输入框展示值。第二个事件参数 `context.fixedNumber` 表示处理过小数位数 `decimalPlaces` 的数字 */ - format?: ( - value: InputNumberValue, - context?: { fixedNumber?: InputNumberValue } - ) => InputNumberValue; + format?: (value: InputNumberValue, context?: { fixedNumber?: InputNumberValue }) => InputNumberValue; /** * 透传 Input 输入框组件全部属性 */ @@ -89,12 +80,12 @@ export interface TdInputNumberProps { * 组件尺寸 * @default medium */ - size?: "small" | "medium" | "large"; + size?: 'small' | 'medium' | 'large'; /** * 文本框状态 * @default default */ - status?: "default" | "success" | "warning" | "error"; + status?: 'default' | 'success' | 'warning' | 'error'; /** * 数值改变步数,可以是小数。如果是大数,请保证数据类型为字符串 * @default 1 @@ -108,7 +99,7 @@ export interface TdInputNumberProps { * 按钮布局 * @default row */ - theme?: "column" | "row" | "normal"; + theme?: 'column' | 'row' | 'normal'; /** * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 */ @@ -124,10 +115,7 @@ export interface TdInputNumberProps { /** * 失去焦点时触发 */ - onBlur?: ( - value: InputNumberValue, - context: { e: FocusEvent } - ) => void; + onBlur?: (value: InputNumberValue, context: { e: FocusEvent }) => void; /** * 值变化时触发,`type` 表示触发本次变化的来源 */ @@ -135,49 +123,30 @@ export interface TdInputNumberProps { /** * 回车键按下时触发 */ - onEnter?: ( - value: InputNumberValue, - context: { e: KeyboardEvent } - ) => void; + onEnter?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; /** * 获取焦点时触发 */ - onFocus?: ( - value: InputNumberValue, - context: { e: FocusEvent } - ) => void; + onFocus?: (value: InputNumberValue, context: { e: FocusEvent }) => void; /** * 键盘按下时触发 */ - onKeydown?: ( - value: InputNumberValue, - context: { e: KeyboardEvent } - ) => void; + onKeydown?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; /** * 按下字符键时触发(keydown -> keypress -> keyup) */ - onKeypress?: ( - value: InputNumberValue, - context: { e: KeyboardEvent } - ) => void; + onKeypress?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; /** * 释放键盘时触发 */ - onKeyup?: ( - value: InputNumberValue, - context: { e: KeyboardEvent } - ) => void; + onKeyup?: (value: InputNumberValue, context: { e: KeyboardEvent }) => void; /** * 最大值或最小值校验结束后触发,`exceed-maximum` 表示超出最大值,`below-minimum` 表示小于最小值 */ - onValidate?: (context: { - error?: "exceed-maximum" | "below-minimum"; - }) => void; + onValidate?: (context: { error?: 'exceed-maximum' | 'below-minimum' }) => void; } -export type InputNumberDecimalPlaces = - | number - | { enableRound: boolean; places: number }; +export type InputNumberDecimalPlaces = number | { enableRound: boolean; places: number }; export type InputNumberValue = number | string; @@ -191,11 +160,4 @@ export interface ChangeContext { | CompositionEvent; } -export type ChangeSource = - | "add" - | "reduce" - | "input" - | "blur" - | "enter" - | "clear" - | "props"; +export type ChangeSource = 'add' | 'reduce' | 'input' | 'blur' | 'enter' | 'clear' | 'props'; \ No newline at end of file diff --git a/packages/components/tag-input/type.ts b/packages/components/tag-input/type.ts index 5ace2ddc73..61361492cc 100644 --- a/packages/components/tag-input/type.ts +++ b/packages/components/tag-input/type.ts @@ -4,17 +4,10 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { InputProps } from "../input"; -import { TagProps } from "../tag"; -import type { TNode, TElement, SizeEnum } from "../common"; -import type { - MouseEvent, - KeyboardEvent, - ClipboardEvent, - FocusEvent, - FormEvent, - CompositionEvent, -} from "react"; +import { InputProps } from '../input'; +import { TagProps } from '../tag'; +import type { TNode, TElement, SizeEnum } from '../common'; +import type { MouseEvent, KeyboardEvent, ClipboardEvent, FocusEvent, FormEvent, CompositionEvent } from 'react'; export interface TdTagInputProps { /** @@ -54,7 +47,7 @@ export interface TdTagInputProps { * 标签超出时的呈现方式,有两种:横向滚动显示 和 换行显示 * @default break-line */ - excessTagsDisplayType?: "scroll" | "break-line"; + excessTagsDisplayType?: 'scroll' | 'break-line'; /** * 标签最大换行数 * @default 1 @@ -114,7 +107,7 @@ export interface TdTagInputProps { /** * 输入框状态 */ - status?: "default" | "success" | "warning" | "error"; + status?: 'default' | 'success' | 'warning' | 'error'; /** * 后置图标前的后置内容 */ @@ -130,11 +123,7 @@ export interface TdTagInputProps { /** * 自定义单个标签的整体节点 */ - tagDisplay?: TNode<{ - value: string | number; - index: number; - onClose: (context?: { e?: MouseEvent }) => void; - }>; + tagDisplay?: TNode<{ value: string | number; index: number; onClose: (context?: { e?: MouseEvent }) => void }>; /** * 透传 Tag 组件全部属性 */ @@ -156,19 +145,11 @@ export interface TdTagInputProps { /** * 自定义值呈现的全部内容,参数为所有标签的值 */ - valueDisplay?: - | string - | TNode<{ - value: TagInputValue; - onClose: (index: number, item?: any) => void; - }>; + valueDisplay?: string | TNode<{ value: TagInputValue; onClose: (index: number, item?: any) => void }>; /** * 失去焦点时触发 */ - onBlur?: ( - value: TagInputValue, - context: { inputValue: string; e: FocusEvent } - ) => void; + onBlur?: (value: TagInputValue, context: { inputValue: string; e: FocusEvent }) => void; /** * 值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数 */ @@ -188,17 +169,11 @@ export interface TdTagInputProps { /** * 按键按下 Enter 时触发 */ - onEnter?: ( - value: TagInputValue, - context: { e: KeyboardEvent; inputValue: string } - ) => void; + onEnter?: (value: TagInputValue, context: { e: KeyboardEvent; inputValue: string }) => void; /** * 聚焦时触发 */ - onFocus?: ( - value: TagInputValue, - context: { inputValue: string; e: FocusEvent } - ) => void; + onFocus?: (value: TagInputValue, context: { inputValue: string; e: FocusEvent }) => void; /** * 输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发、回车键触发等 */ @@ -214,10 +189,7 @@ export interface TdTagInputProps { /** * 粘贴事件,`pasteValue` 表示粘贴板的内容 */ - onPaste?: (context: { - e: ClipboardEvent; - pasteValue: string; - }) => void; + onPaste?: (context: { e: ClipboardEvent; pasteValue: string }) => void; /** * 移除单个标签时触发 */ @@ -233,11 +205,7 @@ export interface TagInputChangeContext { e?: MouseEvent | KeyboardEvent; } -export type TagInputTriggerSource = - | "enter" - | "tag-remove" - | "backspace" - | "clear"; +export type TagInputTriggerSource = 'enter' | 'tag-remove' | 'backspace' | 'clear'; export interface TagInputDragSortContext { newTags: TagInputValue; @@ -253,7 +221,7 @@ export interface InputValueChangeContext { | MouseEvent | CompositionEvent | KeyboardEvent; - trigger: "input" | "clear" | "enter" | "blur"; + trigger: 'input' | 'clear' | 'enter' | 'blur'; } export interface TagInputRemoveContext { @@ -264,4 +232,4 @@ export interface TagInputRemoveContext { trigger: TagInputRemoveTrigger; } -export type TagInputRemoveTrigger = "tag-remove" | "backspace"; +export type TagInputRemoveTrigger = 'tag-remove' | 'backspace'; \ No newline at end of file From c7a6af5066fc2e43360ca1978dabc89eefda5ed7 Mon Sep 17 00:00:00 2001 From: Rylan Date: Thu, 25 Jun 2026 16:46:55 +0800 Subject: [PATCH 16/20] fix: min-width --- packages/components/select-input/useSingle.tsx | 14 +++++++++----- test/snap/__snapshots__/csr.test.jsx.snap | 8 ++++---- test/snap/__snapshots__/ssr.test.jsx.snap | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index cb421dcd80..db170bcadb 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -94,10 +94,14 @@ export default function useSingle(props: SelectInputProps) { }, [props.label, classPrefix]); useEffect(() => { - if (showCustomElement && customElementRef.current) { - const { width } = customElementRef.current.getBoundingClientRect(); - setCustomElementWidth(width); - } + if (!showCustomElement || !customElementRef.current) return; + const el = customElementRef.current; + // 测量真实内容宽度时,临时强制 nowrap,避免被父级 absolute 容器(受 suffixSpace 影响)压缩换行导致测量值偏小 + const prevWhiteSpace = el.style.whiteSpace; + el.style.whiteSpace = 'nowrap'; + const { width } = el.getBoundingClientRect(); + el.style.whiteSpace = prevWhiteSpace; + setCustomElementWidth((prev) => (Math.abs(prev - width) < 0.5 ? prev : width)); }, [showCustomElement, singleValueDisplay]); useEffect(() => { @@ -126,7 +130,7 @@ export default function useSingle(props: SelectInputProps) { const inputRect = inputEl.getBoundingClientRect(); // wrapper 右内边距 + suffix 区域 + suffixIcon 区域 const space = Math.max(wrapperRect.right - inputRect.right, 0); - setSuffixSpace(space); + setSuffixSpace((prev) => (Math.abs(prev - space) < 0.5 ? prev : space)); }; measure(); diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index a0fda9214c..aca828845d 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -22976,7 +22976,7 @@ exports[`csr snapshot test > csr test packages/components/cascader/_example/valu style="position: absolute; left: 8px; right: 0px; top: 50%; transform: translateY(-50%); pointer-events: none; text-align: initial; overflow: hidden;" >
csr test packages/components/select-input/_example/ style="position: absolute; left: 8px; right: 0px; top: 50%; transform: translateY(-50%); pointer-events: none; text-align: initial; overflow: hidden;" > ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
onlyLeaf
请选择
parentFirst
请选择
all
请选择
"`; @@ -154413,7 +154413,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index 1360725cdc..239a6289ec 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -210,7 +210,7 @@ exports[`ssr snapshot test > ssr test packages/components/cascader/_example/size exports[`ssr snapshot test > ssr test packages/components/cascader/_example/trigger.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; +exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-display.tsx 1`] = `"
单选:
(2.2)
多选:
请选择
"`; exports[`ssr snapshot test > ssr test packages/components/cascader/_example/value-mode.tsx 1`] = `"
onlyLeaf
请选择
parentFirst
请选择
all
请选择
"`; @@ -870,7 +870,7 @@ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/ exports[`ssr snapshot test > ssr test packages/components/select-input/_example/collapsed-items.tsx 1`] = `"
tdesign-vue
+5


tdesign-vue
tdesign-react
More(+4)
"`; -exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; +exports[`ssr snapshot test > ssr test packages/components/select-input/_example/custom-tag.tsx 1`] = `"
tdesign-vue


tdesign-vue
tdesign-react


tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
tdesign-vuetdesign-reacttdesign-mobile-vue
"`; exports[`ssr snapshot test > ssr test packages/components/select-input/_example/excess-tags-display-type.tsx 1`] = `"

第一种呈现方式:超出时滚动显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react



第二种呈现方式:超出时换行显示


tdesign-vue
tdesign-react
tdesign-miniprogram
tdesign-angular
tdesign-mobile-vue
tdesign-mobile-react
"`; From 4c002881ae63e5165a8e4c00885c0234c51cf706 Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 26 Jun 2026 13:00:35 +0800 Subject: [PATCH 17/20] fix: calc width --- .../components/select-input/useSingle.tsx | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index db170bcadb..26c9e58d7c 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -96,7 +96,7 @@ export default function useSingle(props: SelectInputProps) { useEffect(() => { if (!showCustomElement || !customElementRef.current) return; const el = customElementRef.current; - // 测量真实内容宽度时,临时强制 nowrap,避免被父级 absolute 容器(受 suffixSpace 影响)压缩换行导致测量值偏小 + // 测量真实内容宽度时,临时强制 nowrap,避免被父级容器(受 suffixSpace 影响)压缩换行导致测量值偏小 const prevWhiteSpace = el.style.whiteSpace; el.style.whiteSpace = 'nowrap'; const { width } = el.getBoundingClientRect(); @@ -104,7 +104,10 @@ export default function useSingle(props: SelectInputProps) { setCustomElementWidth((prev) => (Math.abs(prev - width) < 0.5 ? prev : width)); }, [showCustomElement, singleValueDisplay]); + // 当存在自定义 valueDisplay 时,labelNode 使用 absolute 定位 + // 需要给 input 设置 minWidth 来撑开宽度 useEffect(() => { + // autoWidth 时确保完全显示内容 const inputEl = inputRef.current?.inputElement; if (!inputEl || !autoWidth) return; if (showCustomElement && customElementWidth > 0) { @@ -114,6 +117,34 @@ export default function useSingle(props: SelectInputProps) { } }, [autoWidth, showCustomElement, customElementWidth]); + useEffect(() => { + // 非 autoWidth 时避免覆盖用户在外层设置的宽度约束(width / maxWidth) + // 测量祖先实际可用宽度作为上限 + const wrapperEl = inputRef.current?.currentElement; + if (!wrapperEl || autoWidth) return; + const hasUserDefinedWidth = + props.style?.width || props.inputProps?.style?.width || props.inputProps?.style?.minWidth; + if (hasUserDefinedWidth || !showCustomElement || customElementWidth <= 0) { + wrapperEl.style.minWidth = ''; + return; + } + const width = customElementWidth + labelWidth + 48; + // 先重置自身 minWidth,避免影响父级宽度测量 + wrapperEl.style.minWidth = ''; + const parentEl = wrapperEl.parentElement; + const parentWidth = parentEl ? parentEl.getBoundingClientRect().width : 0; + const finalWidth = parentWidth > 0 ? Math.min(width, parentWidth) : width; + wrapperEl.style.minWidth = `${finalWidth}px`; + }, [ + autoWidth, + showCustomElement, + customElementWidth, + labelWidth, + props.style?.width, + props.inputProps?.style?.width, + props.inputProps?.style?.minWidth, + ]); + useEffect(() => { // 自定义 valueDisplay 时,labelNode 使用绝对定位 // 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白 @@ -223,13 +254,6 @@ export default function useSingle(props: SelectInputProps) {
) : null; - const hasCustomWidth = props.style?.width || props.inputProps?.style?.width || props.inputProps?.style?.minWidth; - // customElement 定位为 absolute,无法撑开 input 宽度 - const inputWidth = - !hasCustomWidth && showCustomElement && customElementWidth > 0 - ? `${customElementWidth + labelWidth + 48}px` - : undefined; - return ( Date: Fri, 26 Jun 2026 13:05:03 +0800 Subject: [PATCH 18/20] chore: remove comments --- packages/components/select-input/useSingle.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 26c9e58d7c..24606ed5af 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -146,7 +146,6 @@ export default function useSingle(props: SelectInputProps) { ]); useEffect(() => { - // 自定义 valueDisplay 时,labelNode 使用绝对定位 // 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白 if (!showCustomElement) { setSuffixSpace(0); From 8fb5fbc7ad4cda842c473b3b8a13b724ed70e114 Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 26 Jun 2026 14:54:00 +0800 Subject: [PATCH 19/20] chore: optimize code --- .../components/select-input/useSingle.tsx | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 24606ed5af..1c0f3ce758 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -45,28 +45,37 @@ function getOptionLabel(value: TdSelectInputProps['value'], keys: TdSelectInputP } export default function useSingle(props: SelectInputProps) { - const { value, loading, autoWidth } = props; + const { value, autoWidth, inputProps, label, allowInput, clearable, keys, valueDisplay, suffixIcon } = props; const commonInputProps: SelectInputCommonProperties = { ...pick(props, COMMON_PROPERTIES), - suffixIcon: loading ? : props.suffixIcon, + suffixIcon: props.loading ? : suffixIcon, }; + const styleProps = props.style || {}; + const inputStyleProps = inputProps?.style || {}; + const hasWidthProps = Boolean( + styleProps.width || + styleProps.minWidth || + styleProps.maxWidth || + inputStyleProps.width || + inputStyleProps.minWidth || + inputStyleProps.maxWidth, + ); + const { classPrefix } = useConfig(); const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); const inputRef = useRef(null); const blurTimeoutRef = useRef(null); const customElementRef = useRef(null); + const minWidthSetRef = useRef(false); // 标记 minWidth 是否由当前 hook 设置,避免误清用户外部设置的数值 const [isTyping, setIsTyping] = useState(false); const [labelWidth, setLabelWidth] = useState(0); const [customElementWidth, setCustomElementWidth] = useState(0); const [suffixSpace, setSuffixSpace] = useState(0); - const singleValueDisplay = useMemo( - () => props.valueDisplay ?? getOptionLabel(value, props.keys), - [value, props.valueDisplay, props.keys], - ); + const singleValueDisplay = useMemo(() => valueDisplay ?? getOptionLabel(value, keys), [value, valueDisplay, keys]); const showCustomElement = useMemo( () => !isTyping && !inputValue && React.isValidElement(singleValueDisplay), @@ -80,7 +89,7 @@ export default function useSingle(props: SelectInputProps) { }; const onInnerInputChange: TdInputProps['onChange'] = (value, context) => { - if (props.allowInput) { + if (allowInput) { setInputValue(value, { ...context, trigger: 'input' }); } }; @@ -91,7 +100,7 @@ export default function useSingle(props: SelectInputProps) { const prefixWidth = labelEl.getBoundingClientRect().width; setLabelWidth(prefixWidth); } - }, [props.label, classPrefix]); + }, [label, classPrefix]); useEffect(() => { if (!showCustomElement || !customElementRef.current) return; @@ -118,32 +127,30 @@ export default function useSingle(props: SelectInputProps) { }, [autoWidth, showCustomElement, customElementWidth]); useEffect(() => { - // 非 autoWidth 时避免覆盖用户在外层设置的宽度约束(width / maxWidth) + // 非 autoWidth 时避免覆盖用户在外层设置的宽度约束(width / minWidth / maxWidth) // 测量祖先实际可用宽度作为上限 - const wrapperEl = inputRef.current?.currentElement; + const wrapperEl = inputRef.current?.currentElement as HTMLElement | undefined; if (!wrapperEl || autoWidth) return; - const hasUserDefinedWidth = - props.style?.width || props.inputProps?.style?.width || props.inputProps?.style?.minWidth; - if (hasUserDefinedWidth || !showCustomElement || customElementWidth <= 0) { - wrapperEl.style.minWidth = ''; + const clearMinWidth = () => { + // 仅当 minWidth 是由当前 hook 设置时才清除,避免误清用户外部设置的冲突 + if (minWidthSetRef.current) { + wrapperEl.style.minWidth = ''; + minWidthSetRef.current = false; + } + }; + if (hasWidthProps || !showCustomElement || customElementWidth <= 0) { + clearMinWidth(); return; } const width = customElementWidth + labelWidth + 48; - // 先重置自身 minWidth,避免影响父级宽度测量 - wrapperEl.style.minWidth = ''; + // 先临时重置设置的 minWidth,避免影响宽度测量 + clearMinWidth(); const parentEl = wrapperEl.parentElement; const parentWidth = parentEl ? parentEl.getBoundingClientRect().width : 0; const finalWidth = parentWidth > 0 ? Math.min(width, parentWidth) : width; wrapperEl.style.minWidth = `${finalWidth}px`; - }, [ - autoWidth, - showCustomElement, - customElementWidth, - labelWidth, - props.style?.width, - props.inputProps?.style?.width, - props.inputProps?.style?.minWidth, - ]); + minWidthSetRef.current = true; + }, [autoWidth, showCustomElement, customElementWidth, labelWidth, hasWidthProps]); useEffect(() => { // 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白 @@ -171,7 +178,7 @@ export default function useSingle(props: SelectInputProps) { wrapperEl.removeEventListener('mouseenter', measure); wrapperEl.removeEventListener('mouseleave', measure); }; - }, [showCustomElement, singleValueDisplay, props.clearable, props.suffixIcon, props.suffix]); + }, [showCustomElement, singleValueDisplay, clearable, suffixIcon, props.suffix]); const renderSelectSingle = ( popupVisible: boolean, @@ -208,7 +215,7 @@ export default function useSingle(props: SelectInputProps) { if (popupVisible && inputValue) { return inputValue; } - if (props.allowInput && popupVisible && !showCustomElement) { + if (allowInput && popupVisible && !showCustomElement) { return ''; } if (!showCustomElement) { @@ -237,7 +244,7 @@ export default function useSingle(props: SelectInputProps) { textAlign: 'initial', overflow: 'hidden', // 输入状态,降低透明度,仿造 placeholder 效果 - opacity: popupVisible && props.allowInput ? 0.5 : undefined, + opacity: popupVisible && allowInput ? 0.5 : undefined, }} > { setIsTyping(true); - props.inputProps?.onCompositionstart?.(v, ctx); + inputProps?.onCompositionstart?.(v, ctx); }} onCompositionend={(v, ctx) => { setIsTyping(false); - props.inputProps?.onCompositionend?.(v, ctx); + inputProps?.onCompositionend?.(v, ctx); }} - inputClass={classNames(props.inputProps?.inputClass, { + inputClass={classNames(inputProps?.inputClass, { [`${classPrefix}-input--focused`]: popupVisible, [`${classPrefix}-is-focused`]: popupVisible, })} From 66f3a46e69544f6428dabc1e2fe12274fba0083e Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 26 Jun 2026 15:48:34 +0800 Subject: [PATCH 20/20] fix: simplify logic --- .../components/select-input/useSingle.tsx | 68 +++---------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 1c0f3ce758..d5a09adbbd 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -39,10 +39,10 @@ const DEFAULT_KEYS: TdSelectInputProps['keys'] = { value: 'value', }; -function getOptionLabel(value: TdSelectInputProps['value'], keys: TdSelectInputProps['keys']) { +const getOptionLabel = (value: TdSelectInputProps['value'], keys: TdSelectInputProps['keys']) => { const iKeys = keys || DEFAULT_KEYS; return isObject(value) ? value[iKeys.label] : value; -} +}; export default function useSingle(props: SelectInputProps) { const { value, autoWidth, inputProps, label, allowInput, clearable, keys, valueDisplay, suffixIcon } = props; @@ -51,28 +51,15 @@ export default function useSingle(props: SelectInputProps) { suffixIcon: props.loading ? : suffixIcon, }; - const styleProps = props.style || {}; - const inputStyleProps = inputProps?.style || {}; - const hasWidthProps = Boolean( - styleProps.width || - styleProps.minWidth || - styleProps.maxWidth || - inputStyleProps.width || - inputStyleProps.minWidth || - inputStyleProps.maxWidth, - ); - const { classPrefix } = useConfig(); const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); const inputRef = useRef(null); const blurTimeoutRef = useRef(null); const customElementRef = useRef(null); - const minWidthSetRef = useRef(false); // 标记 minWidth 是否由当前 hook 设置,避免误清用户外部设置的数值 const [isTyping, setIsTyping] = useState(false); const [labelWidth, setLabelWidth] = useState(0); - const [customElementWidth, setCustomElementWidth] = useState(0); const [suffixSpace, setSuffixSpace] = useState(0); const singleValueDisplay = useMemo(() => valueDisplay ?? getOptionLabel(value, keys), [value, valueDisplay, keys]); @@ -103,54 +90,21 @@ export default function useSingle(props: SelectInputProps) { }, [label, classPrefix]); useEffect(() => { - if (!showCustomElement || !customElementRef.current) return; + const inputEl = inputRef.current?.inputElement; + if (!inputEl) return; + // autoWidth 且存在自定义元素时需要撑开宽度 + if (!autoWidth || !showCustomElement || !customElementRef.current) { + inputEl.style.minWidth = ''; + return; + } const el = customElementRef.current; // 测量真实内容宽度时,临时强制 nowrap,避免被父级容器(受 suffixSpace 影响)压缩换行导致测量值偏小 const prevWhiteSpace = el.style.whiteSpace; el.style.whiteSpace = 'nowrap'; const { width } = el.getBoundingClientRect(); el.style.whiteSpace = prevWhiteSpace; - setCustomElementWidth((prev) => (Math.abs(prev - width) < 0.5 ? prev : width)); - }, [showCustomElement, singleValueDisplay]); - - // 当存在自定义 valueDisplay 时,labelNode 使用 absolute 定位 - // 需要给 input 设置 minWidth 来撑开宽度 - useEffect(() => { - // autoWidth 时确保完全显示内容 - const inputEl = inputRef.current?.inputElement; - if (!inputEl || !autoWidth) return; - if (showCustomElement && customElementWidth > 0) { - inputEl.style.minWidth = `${customElementWidth}px`; - } else { - inputEl.style.minWidth = ''; - } - }, [autoWidth, showCustomElement, customElementWidth]); - - useEffect(() => { - // 非 autoWidth 时避免覆盖用户在外层设置的宽度约束(width / minWidth / maxWidth) - // 测量祖先实际可用宽度作为上限 - const wrapperEl = inputRef.current?.currentElement as HTMLElement | undefined; - if (!wrapperEl || autoWidth) return; - const clearMinWidth = () => { - // 仅当 minWidth 是由当前 hook 设置时才清除,避免误清用户外部设置的冲突 - if (minWidthSetRef.current) { - wrapperEl.style.minWidth = ''; - minWidthSetRef.current = false; - } - }; - if (hasWidthProps || !showCustomElement || customElementWidth <= 0) { - clearMinWidth(); - return; - } - const width = customElementWidth + labelWidth + 48; - // 先临时重置设置的 minWidth,避免影响宽度测量 - clearMinWidth(); - const parentEl = wrapperEl.parentElement; - const parentWidth = parentEl ? parentEl.getBoundingClientRect().width : 0; - const finalWidth = parentWidth > 0 ? Math.min(width, parentWidth) : width; - wrapperEl.style.minWidth = `${finalWidth}px`; - minWidthSetRef.current = true; - }, [autoWidth, showCustomElement, customElementWidth, labelWidth, hasWidthProps]); + inputEl.style.minWidth = width > 0 ? `${width}px` : ''; + }, [autoWidth, showCustomElement, singleValueDisplay]); useEffect(() => { // 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白