-
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 all 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
| +5 −2 | scripts/generate-css-vars.mjs | |
| +7 −0 | style/mobile/components/image-viewer/_index.less | |
| +2 −2 | style/mobile/components/navbar/_index.less |
| 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'; | ||
|
|
@@ -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 ? <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 customElementRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| const [inputValue, setInputValue] = useControlled(props, 'inputValue', props.onInputChange); | ||
| const [isTyping, setIsTyping] = useState<boolean>(false); | ||
| const [labelWidth, setLabelWidth] = useState<number>(0); | ||
| const [customElementWidth, setCustomElementWidth] = 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 +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<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 +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 ? ( | ||
| <div | ||
| ref={customElementRef} | ||
| 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 + 8}px`, | ||
| top: '50%', | ||
| transform: 'translateY(-50%)', | ||
| pointerEvents: 'none', | ||
| textAlign: 'initial', | ||
| zIndex: 3, | ||
| // 输入状态,降低透明度,仿造 placeholder 效果 | ||
| opacity: popupVisible && props.allowInput ? 0.5 : undefined, | ||
| }} | ||
| > | ||
| {singleValueDisplay} | ||
| </div> | ||
| ) : 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 ( | ||
| <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} | ||
| 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, | ||
| })} | ||
|
|
||
| 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; |


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