Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/components/_util/parseTNode.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -37,3 +37,17 @@ export function parseContentTNode<T>(tnode: TNode<T>, 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 '';
}
89 changes: 71 additions & 18 deletions packages/components/select-input/useSingle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';

import classNames from 'classnames';
import { isObject, pick } from 'lodash-es';
Expand Down Expand Up @@ -38,19 +38,25 @@ 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;
const { value, loading } = props;

const optionLabel = getOptionLabel(value, props.keys);
const singleValueDisplay = props.valueDisplay ?? optionLabel;
const showLabelNode = React.isValidElement(singleValueDisplay);

const { classPrefix } = useConfig();
const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange);

const inputRef = useRef<InputRef>(null);
const blurTimeoutRef = useRef(null);

const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange);
const [labelWidth, setLabelWidth] = useState<number>(0);

const commonInputProps: SelectInputCommonProperties = {
...pick(props, COMMON_PROPERTIES),
Expand All @@ -69,14 +75,18 @@ 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<HTMLInputElement> }) => 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);
Expand Down Expand Up @@ -104,22 +114,65 @@ 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 && !showLabelNode) {
return '';
}
if (!showLabelNode) {
return singleValueDisplay;
}
return inputValue;
};

const displayedPlaceholder = () => {
if (popupVisible && singleValueDisplay && !showLabelNode) {
return singleValueDisplay;
}
if (showLabelNode) return '';
return props.placeholder;
};

const labelNode = showLabelNode ? (
<div
style={{
position: 'absolute',
left: `${labelWidth + 16}px`,
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
textAlign: 'initial',
zIndex: 3,
// 输入状态,降低透明度,仿造 placeholder 效果
opacity: popupVisible && props.allowInput ? 0.5 : undefined,
}}
>
{singleValueDisplay}
</div>
) : null;

return (
<Input
ref={inputRef}
// 当 label 为 自定义节点时,input 为空,确保此时 clear icon 可见
showClearIconOnEmpty={props.clearable && showLabelNode}
{...commonInputProps}
autoWidth={props.autoWidth}
allowInput={props.allowInput}
placeholder={singleValueDisplay ? '' : props.placeholder}
value={singleValueDisplay ? ' ' : displayedValue}
label={
(props.label || singleValueDisplay) && (
suffix={
labelNode ||
(commonInputProps.suffix && (
<>
{props.label}
{singleValueDisplay as React.ReactNode}
{labelNode}
{commonInputProps.suffix}
</>
)
))
}
autoWidth={props.autoWidth}
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
Expand All @@ -130,7 +183,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,
})}
Expand Down
104 changes: 61 additions & 43 deletions packages/components/select/_example/custom-options.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div style={{ display: 'flex', padding: '8px 0' }}>
<img
src="https://tdesign.gtimg.com/site/avatar.jpg"
style={{
maxWidth: '40px',
borderRadius: '50%',
}}
/>
<div style={{ marginLeft: '16px' }}>
<div>用户{index}</div>
<div
style={{
fontSize: '13px',
color: 'var(--td-gray-color-9)',
}}
>
这是一段用户描述信息,可自定义内容
</div>
</div>
</div>
);

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 (
<Select value={value} onChange={onChange} style={{ width: '300px' }} clearable>
{options.map((option, idx) => (
<Option style={{ height: '60px' }} key={idx} value={option.value} label={option.label}>
<div style={{ display: 'flex' }}>
<img
src={avatarUrl}
style={{
maxWidth: '40px',
borderRadius: '50%',
}}
/>
<div style={{ marginLeft: '16px' }}>
<div>{option.label}</div>
<div
style={{
fontSize: '13px',
color: 'var(--td-gray-color-9)',
}}
>
{option.description}
</div>
</div>
</div>
</Option>
))}
</Select>
<Space size="150px">
<Space direction="vertical">
<strong>法一:使用插槽</strong>
<Select value={value} onChange={onChange} clearable>
{options1.map((option, idx) => (
<Option style={{ height: '60px' }} key={idx} value={option.value} label={option.label}>
{generateCustomContent(idx + 1)}
</Option>
))}
</Select>
</Space>
<Space direction="vertical">
<strong>法二:使用 `content` 属性</strong>
<Select options={options2} value={value} onChange={onChange} clearable style={{ width: 200 }} />
</Space>
</Space>
);
}

export default CustomOptions;
13 changes: 9 additions & 4 deletions packages/components/select/base/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ const Option: React.FC<SelectOptionProps> = (props) => {
const label = propLabel || value;
const disabled = propDisabled || (multiple && Array.isArray(selectedValue) && max && selectedValue.length >= max);

const displayedContent = children || content || label;
const isCustomElement = React.isValidElement(displayedContent);

const titleContent = useMemo(() => {
// 外部设置 props,说明希望受控
const controlledTitle = Reflect.has(props, 'title');
Expand Down Expand Up @@ -121,7 +124,6 @@ const Option: React.FC<SelectOptionProps> = (props) => {
};

const renderItem = () => {
const displayContent = children || content || label;
if (multiple) {
return (
<label
Expand All @@ -143,11 +145,11 @@ const Option: React.FC<SelectOptionProps> = (props) => {
}}
/>
<span className={classNames(`${classPrefix}-checkbox__input`)}></span>
<span className={classNames(`${classPrefix}-checkbox__label`)}>{displayContent}</span>
<span className={classNames(`${classPrefix}-checkbox__label`)}>{displayedContent}</span>
</label>
);
}
return <span title={titleContent}>{displayContent}</span>;
return <span title={titleContent}>{displayedContent}</span>;
};

return (
Expand All @@ -161,7 +163,10 @@ const Option: React.FC<SelectOptionProps> = (props) => {
key={value}
onClick={handleSelect}
ref={setRefCurrent}
style={style}
style={{
...(isCustomElement ? { height: 'auto' } : {}),
...style,
}}
>
{renderItem()}
</li>
Expand Down
1 change: 0 additions & 1 deletion packages/components/select/base/OptionGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const OptionGroup: React.FC<SelectGOptionGroupProps> = (props) => {
{children}
</li>
);
return;
};

export default OptionGroup;
Loading
Loading