diff --git a/packages/components/_util/parseTNode.ts b/packages/components/_util/parseTNode.ts index 18b04d580f..ea9db5040f 100644 --- a/packages/components/_util/parseTNode.ts +++ b/packages/components/_util/parseTNode.ts @@ -39,3 +39,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/cascader/components/Item.tsx b/packages/components/cascader/components/Item.tsx index 2a750e2d36..6110a8abf6 100644 --- a/packages/components/cascader/components/Item.tsx +++ b/packages/components/cascader/components/Item.tsx @@ -89,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 = ( diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 395e310c31..03ec88216b 100644 --- a/packages/components/select-input/useSingle.tsx +++ b/packages/components/select-input/useSingle.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { isObject, pick } from 'lodash-es'; @@ -9,6 +9,7 @@ import Loading from '../loading'; import type { InputRef, TdInputProps } from '../input'; import type { SelectInputCommonProperties } from './interface'; +import type { SelectInputProps } from './SelectInput'; import type { TdSelectInputProps } from './type'; export interface RenderSelectSingleInputParams { @@ -38,24 +39,39 @@ const DEFAULT_KEYS: TdSelectInputProps['keys'] = { value: 'value', }; -function getInputValue(value: TdSelectInputProps['value'], keys: TdSelectInputProps['keys']) { +function getOptionLabel(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; +export default function useSingle(props: SelectInputProps) { + const { value, loading } = props; + const commonInputProps: SelectInputCommonProperties = { + ...pick(props, COMMON_PROPERTIES), + suffixIcon: loading ? : props.suffixIcon, + }; + const { classPrefix } = useConfig(); + const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); const inputRef = useRef(null); const blurTimeoutRef = useRef(null); + const customElementRef = useRef(null); - const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); + const [isTyping, setIsTyping] = useState(false); + const [labelWidth, setLabelWidth] = useState(0); + const [customElementWidth, setCustomElementWidth] = useState(0); + const [suffixSpace, setSuffixSpace] = 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 showCustomElement = useMemo( + () => !isTyping && !inputValue && React.isValidElement(singleValueDisplay), + [isTyping, inputValue, singleValueDisplay], + ); const onInnerClear = (context: { e: React.MouseEvent }) => { context?.e?.stopPropagation(); @@ -69,14 +85,64 @@ 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]); + + useEffect(() => { + if (showCustomElement && customElementRef.current) { + const { width } = customElementRef.current.getBoundingClientRect(); + setCustomElementWidth(width); + } + }, [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, ) => { - // 单选,值的呈现方式 - const singleValueDisplay: any = !props.multiple ? props.valueDisplay : null; - const displayedValue = popupVisible && props.allowInput ? inputValue : getInputValue(value, keys); - const handleBlur = (value, ctx) => { if (blurTimeoutRef.current) { clearTimeout(blurTimeoutRef.current); @@ -104,22 +170,79 @@ export default function useSingle(props: TdSelectInputProps) { // !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' }); }; + const displayedValue = (): string => { + if (popupVisible && inputValue) { + return inputValue; + } + if (props.allowInput && popupVisible && !showCustomElement) { + return ''; + } + if (!showCustomElement) { + return singleValueDisplay; + } + return inputValue; + }; + + const displayedPlaceholder = (): string => { + if (popupVisible && singleValueDisplay && !showCustomElement) { + return singleValueDisplay; + } + if (showCustomElement) return ''; + return props.placeholder; + }; + + const labelNode = showCustomElement ? ( +
+ + {singleValueDisplay} + +
+ ) : 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 ( - {props.label} - {singleValueDisplay as React.ReactNode} + {labelNode} + {commonInputProps.suffix} - ) + )) } + autoWidth={props.autoWidth} + style={{ + ...(props.inputProps?.style || {}), + minWidth: inputWidth, + }} + allowInput={props.allowInput} + 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 +253,15 @@ 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, { + onCompositionstart={(v, ctx) => { + 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, })} diff --git a/packages/components/select/_example/custom-options.tsx b/packages/components/select/_example/custom-options.tsx index b5883538a9..4a17df8594 100644 --- a/packages/components/select/_example/custom-options.tsx +++ b/packages/components/select/_example/custom-options.tsx @@ -1,54 +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 + ) + +
+
+
+
@@ -33663,7 +33675,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\` 属性 + +
+
+
+
+
+
+ + + + + + +
+
+
+
@@ -89176,22 +89276,17 @@ exports[`csr snapshot test > csr test packages/components/select/_example/custom
-
- 选中选项一 -
csr test packages/components/select-input/_example/
-
- - - - - - - - tdesign-vue - -
+
+
+ + + + + + + + + tdesign-vue + + +
+
@@ -148904,7 +149006,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`] = `"
onlyLeaf
请选择
parentFirst
请选择
all
请选择
"`; @@ -153436,7 +153531,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
"`; @@ -153980,7 +154075,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`] = `"
请选择
"`; @@ -154032,7 +154127,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
"`; @@ -154394,7 +154489,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`] = `"
"`; @@ -154404,11 +154499,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 46e9f02c8d..d3bd37c05e 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
请选择
"`; @@ -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
"`; @@ -818,7 +818,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`] = `"
请选择
"`; @@ -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
"`; @@ -1232,7 +1232,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`] = `"
"`; @@ -1242,11 +1242,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`] = `"
广州市
深圳市
"`;