diff --git a/packages/common b/packages/common index 3727d58bc9..054ebf90bb 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 3727d58bc953ce6664d40bdb04c8f5dd813cf169 +Subproject commit 054ebf90bbed57efb922ff007471ba18cb53a2e8 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/cascader/components/Item.tsx b/packages/components/cascader/components/Item.tsx index f21377524e..eaea149737 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 = ( diff --git a/packages/components/select-input/useSingle.tsx b/packages/components/select-input/useSingle.tsx index 9e92324767..d0e487cde7 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 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 { @@ -38,24 +39,38 @@ 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 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 +84,25 @@ 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]); + 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 +130,77 @@ export default function useSingle(props: TdSelectInputProps) { // !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' }); }; + const displayedValue = () => { + if (popupVisible && inputValue) { + return inputValue; + } + if (props.allowInput && popupVisible && !showCustomElement) { + return ''; + } + if (!showCustomElement) { + return singleValueDisplay; + } + return inputValue; + }; + + const displayedPlaceholder = () => { + 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 +211,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 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 + ) + +
+
+
@@ -33705,7 +33713,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\` 属性 + +
+
+
+
+
+
+ + + + + + +
+
+
+
@@ -87352,22 +87448,17 @@ exports[`csr snapshot test > csr test packages/components/select/_example/custom
-
- 选中选项一 -
csr test packages/components/select-input/_example/
-
- - - - - - - - tdesign-vue - -
+
+
+ + + + + + + + tdesign-vue + +
+
@@ -146146,7 +146240,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
请选择
"`; @@ -150678,7 +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`] = `"
"`; -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
"`; @@ -151220,7 +151307,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`] = `"
请选择
"`; @@ -151272,7 +151359,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
"`; @@ -151634,7 +151721,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`] = `"
"`; @@ -151644,11 +151731,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 9d84b8b498..bde8cbcf7b 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
"`; @@ -816,7 +816,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`] = `"
请选择
"`; @@ -868,7 +868,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
"`; @@ -1230,7 +1230,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`] = `"
"`; @@ -1240,11 +1240,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`] = `"
广州市
深圳市
"`;