-
Notifications
You must be signed in to change notification settings - Fork 361
fix(Select): improved label filtering #3891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 6 commits
0c9e573
a95f2db
8ec8e7d
259ed00
ba42885
5766fce
105862d
58394d5
fb71e06
f784914
5e3edce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; | ||
|
|
@@ -38,24 +38,36 @@ 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 commonInputProps: SelectInputCommonProperties = { | ||
| ...pick(props, COMMON_PROPERTIES), | ||
| suffixIcon: loading ? <Loading loading size="small" /> : props.suffixIcon, | ||
| }; | ||
|
|
||
| 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 [isTyping, setIsTyping] = useState<boolean>(false); | ||
| const [labelWidth, setLabelWidth] = useState<number>(0); | ||
|
|
||
| const commonInputProps: SelectInputCommonProperties = { | ||
| ...pick(props, COMMON_PROPERTIES), | ||
| suffixIcon: loading ? <Loading loading size="small" /> : 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<SVGSVGElement> }) => { | ||
| context?.e?.stopPropagation(); | ||
|
|
@@ -69,14 +81,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); | ||
|
|
@@ -104,22 +120,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 && !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 ? ( | ||
| <div | ||
| style={{ | ||
| position: 'absolute', | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| left: `${labelWidth + 16}px`, | ||
RylanBot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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} | ||
| // 当 valueDisplay 为 自定义元素时,选中内容时 input 依旧为空,确保此时 clear icon 可见 | ||
| showClearIconOnEmpty={props.clearable && showCustomElement} | ||
| {...commonInputProps} | ||
| autoWidth={props.autoWidth} | ||
| allowInput={props.allowInput} | ||
| placeholder={singleValueDisplay ? '' : props.placeholder} | ||
| value={singleValueDisplay ? ' ' : displayedValue} | ||
| label={ | ||
| (props.label || singleValueDisplay) && ( | ||
| suffix={ | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 原本的 或者为了减少 DOM 结构的变更 |
||
| 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 | ||
|
|
@@ -130,7 +189,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, | ||
| })} | ||
|
|
||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
|
|
@@ -121,7 +124,6 @@ const Option: React.FC<SelectOptionProps> = (props) => { | |
| }; | ||
|
|
||
| const renderItem = () => { | ||
| const displayContent = children || content || label; | ||
| if (multiple) { | ||
| return ( | ||
| <label | ||
|
|
@@ -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 ( | ||
|
|
@@ -161,7 +163,10 @@ const Option: React.FC<SelectOptionProps> = (props) => { | |
| key={value} | ||
| onClick={handleSelect} | ||
| ref={setRefCurrent} | ||
| style={style} | ||
| style={{ | ||
| ...(isCustomElement ? { height: 'auto' } : {}), | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ...style, | ||
| }} | ||
| > | ||
| {renderItem()} | ||
| </li> | ||
|
|
||



Uh oh!
There was an error while loading. Please reload this page.