From c75700d94f5dee31323daba81db5184d5673aa07 Mon Sep 17 00:00:00 2001 From: En Yang Du <377153400@qq.com> Date: Wed, 25 Sep 2024 11:31:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(select-input):=20=E5=AE=8C=E6=88=90sel?= =?UTF-8?q?ect-input&=E4=BC=98=E5=8C=96popup=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(icon): 修复icon不居中显示和class重复设置问题 * fix(icon): 修复icon不居中显示和class重复设置问题 * fix(icon): 修复icon不居中显示和class重复设置问题 * fix(watermark): 修复watermark无法生成水印及其它展示问题 * fix(message): 临时使用console代替 * feat(select-input): 增加select-input组件 * refactor(input): restore input * perf(popup): 优化popup dom结构 * feat(popup): 优化popup * feat(input-select): add input-select * fix(omi-transition): 动画enter、leave多次同时运行问题修复 * feat(select-input): add select-input * feat(select-input): add select-input * feat(select-input): 去除--force * feat(select-input): 优化css字符串 --- package-lock.json | 14 +- package.json | 2 +- site/sidebar.config.ts | 7 + src/_util/useControlled.ts | 53 ++++ src/button/button.tsx | 9 +- src/checkbox/checkbox-group.tsx | 34 ++- src/checkbox/index.ts | 2 + src/checkbox/type.ts | 4 + src/common.ts | 4 + src/common/portal.tsx | 4 + src/index.ts | 1 + src/input/_example/base.tsx | 1 + src/input/input.tsx | 32 ++- src/popup/_example/placement.tsx | 70 ++---- src/popup/popup.tsx | 169 ++++++++----- src/select-input/README.md | 143 +++++++++++ src/select-input/SelectInput.tsx | 225 +++++++++++++++++ src/select-input/SelectInputMultiple.tsx | 134 ++++++++++ src/select-input/SelectInputSingle.tsx | 113 +++++++++ src/select-input/_example/autocomplete.tsx | 93 +++++++ .../_example/autowidth-multiple.tsx | 133 ++++++++++ src/select-input/_example/autowidth.tsx | 100 ++++++++ .../_example/borderless-multiple.tsx | 137 +++++++++++ src/select-input/_example/borderless.tsx | 95 +++++++ src/select-input/_example/collapsed-items.tsx | 155 ++++++++++++ src/select-input/_example/custom-tag.tsx | 165 +++++++++++++ .../_example/excess-tags-display-type.tsx | 158 ++++++++++++ src/select-input/_example/label-suffix.tsx | 126 ++++++++++ src/select-input/_example/multiple.tsx | 198 +++++++++++++++ src/select-input/_example/single.tsx | 94 +++++++ src/select-input/_example/status.tsx | 103 ++++++++ src/select-input/_example/width.tsx | 88 +++++++ src/select-input/defaultProps.ts | 19 ++ src/select-input/index.tsx | 9 + src/select-input/interface.ts | 18 ++ src/select-input/style/index.js | 9 + src/select-input/type.ts | 231 ++++++++++++++++++ src/select-input/useOverlayInnerStyle.ts | 99 ++++++++ src/tag-input/style/index.js | 9 + src/tag-input/style/index.ts | 15 -- src/tag-input/tag-input.tsx | 28 ++- 41 files changed, 2943 insertions(+), 160 deletions(-) create mode 100644 src/_util/useControlled.ts create mode 100644 src/select-input/README.md create mode 100644 src/select-input/SelectInput.tsx create mode 100644 src/select-input/SelectInputMultiple.tsx create mode 100644 src/select-input/SelectInputSingle.tsx create mode 100644 src/select-input/_example/autocomplete.tsx create mode 100644 src/select-input/_example/autowidth-multiple.tsx create mode 100644 src/select-input/_example/autowidth.tsx create mode 100644 src/select-input/_example/borderless-multiple.tsx create mode 100644 src/select-input/_example/borderless.tsx create mode 100644 src/select-input/_example/collapsed-items.tsx create mode 100644 src/select-input/_example/custom-tag.tsx create mode 100644 src/select-input/_example/excess-tags-display-type.tsx create mode 100644 src/select-input/_example/label-suffix.tsx create mode 100644 src/select-input/_example/multiple.tsx create mode 100644 src/select-input/_example/single.tsx create mode 100644 src/select-input/_example/status.tsx create mode 100644 src/select-input/_example/width.tsx create mode 100644 src/select-input/defaultProps.ts create mode 100644 src/select-input/index.tsx create mode 100644 src/select-input/interface.ts create mode 100644 src/select-input/style/index.js create mode 100644 src/select-input/type.ts create mode 100644 src/select-input/useOverlayInnerStyle.ts create mode 100644 src/tag-input/style/index.js delete mode 100644 src/tag-input/style/index.ts diff --git a/package-lock.json b/package-lock.json index 3c64f38..20a9aaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "copy-to-clipboard": "^3.3.3", "lodash": "~4.17.15", "omi": "^7.7.1", - "omi-transition": "^0.1.8", + "omi-transition": "^0.1.10", "tailwind-merge": "^2.2.1", "tdesign-icons-web-components": "^0.1.4" }, @@ -9723,9 +9723,9 @@ } }, "node_modules/omi-transition": { - "version": "0.1.8", - "resolved": "https://mirrors.tencent.com/npm/omi-transition/-/omi-transition-0.1.8.tgz", - "integrity": "sha512-5OncdwZSDoczL6WtLxirl2L4BP3UPL6UvvtlTNHOGJZ4HmJ6MOMxvTCiafEzewVL0Yq1MGSBreLZwJRLG6JDJQ==", + "version": "0.1.10", + "resolved": "https://mirrors.tencent.com/npm/omi-transition/-/omi-transition-0.1.10.tgz", + "integrity": "sha512-xWICoQ6uaNdrdN4hpTL4BXyqxKB4AcY6ehjmTWOFNDzywerC6hXcmW5mVrVLMlqJdf1MXvVbU7bHeUEaK/hQLw==", "dependencies": { "omi": "latest" } @@ -21009,9 +21009,9 @@ } }, "omi-transition": { - "version": "0.1.8", - "resolved": "https://mirrors.tencent.com/npm/omi-transition/-/omi-transition-0.1.8.tgz", - "integrity": "sha512-5OncdwZSDoczL6WtLxirl2L4BP3UPL6UvvtlTNHOGJZ4HmJ6MOMxvTCiafEzewVL0Yq1MGSBreLZwJRLG6JDJQ==", + "version": "0.1.10", + "resolved": "https://mirrors.tencent.com/npm/omi-transition/-/omi-transition-0.1.10.tgz", + "integrity": "sha512-xWICoQ6uaNdrdN4hpTL4BXyqxKB4AcY6ehjmTWOFNDzywerC6hXcmW5mVrVLMlqJdf1MXvVbU7bHeUEaK/hQLw==", "requires": { "omi": "latest" } diff --git a/package.json b/package.json index 9143d12..888260a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "copy-to-clipboard": "^3.3.3", "lodash": "~4.17.15", "omi": "^7.7.1", - "omi-transition": "^0.1.8", + "omi-transition": "^0.1.10", "tailwind-merge": "^2.2.1", "tdesign-icons-web-components": "^0.1.4" }, diff --git a/site/sidebar.config.ts b/site/sidebar.config.ts index 3633767..e637087 100644 --- a/site/sidebar.config.ts +++ b/site/sidebar.config.ts @@ -154,6 +154,13 @@ export default [ path: '/components/range-input', component: () => import('tdesign-web-components/range-input/README.md'), }, + { + title: 'SelectInput 筛选器输入框', + name: 'select-input', + docType: 'form', + path: '/components/select-input', + component: () => import('tdesign-web-components/select-input/README.md'), + }, { title: 'TagInput 标签输入框', name: ' tag-input', diff --git a/src/_util/useControlled.ts b/src/_util/useControlled.ts new file mode 100644 index 0000000..e866a9b --- /dev/null +++ b/src/_util/useControlled.ts @@ -0,0 +1,53 @@ +import upperFirst from 'lodash/upperFirst'; +import { Component, setActiveComponent, signal, SignalValue } from 'omi'; + +export interface ChangeHandler { + (value: T, ...args: P); +} + +type Defaultoptions = `default${Capitalize}`; + +type ToString = T extends string ? T : `${Extract}`; + +const useControlled:

( + props: R, + valueKey: K, + onChange: ChangeHandler, + defaultOptions?: { + [key in Defaultoptions>]?: R[K]; + } & { [key: string]: any; activeComponent?: Component }, +) => [SignalValue | R[K], ChangeHandler] = ( + // eslint-disable-next-line default-param-last + props = {} as any, + valueKey, + onChange, + defaultOptions = {}, +) => { + // 外部设置 props,说明希望受控 + const controlled = Reflect.has(props, valueKey); + // 受控属性 + const value = props[valueKey]; + // 约定受控属性的非受控 key 为 defaultXxx,某些条件下要在运行时确定 defaultXxx 则通过 defaultOptions 来覆盖 + const defaultValue = + defaultOptions[`default${upperFirst(valueKey as string)}`] || props[`default${upperFirst(valueKey as string)}`]; + + // 受控模式 + if (controlled) return [value, onChange || (() => {})]; + + // 无论是否受控,都要维护一个内部变量,默认值由 defaultValue 控制 + const internalValue = signal(defaultValue); + if (defaultOptions.activeComponent) { + setActiveComponent(defaultOptions.activeComponent); + } + + // 非受控模式 + return [ + internalValue.value, + (newValue, ...args) => { + internalValue.value = newValue; + onChange?.(newValue, ...args); + }, + ]; +}; + +export default useControlled; diff --git a/src/button/button.tsx b/src/button/button.tsx index 2f231dd..30cbb8f 100644 --- a/src/button/button.tsx +++ b/src/button/button.tsx @@ -32,6 +32,7 @@ export default class Button extends Component { content: [String, Object], onClick: Function, ignoreAttributes: Array, + innerStyle: String, }; static defaultProps = { @@ -73,7 +74,6 @@ export default class Button extends Component { render(props: ButtonProps) { const { icon, - className, variant, size, block, @@ -84,10 +84,14 @@ export default class Button extends Component { ignoreAttributes, children, suffix, + innerClass, + innerStyle, ...rest } = props; delete rest.onClick; + delete rest.className; + delete rest.style; const classPrefix = getClassPrefix(); @@ -104,7 +108,7 @@ export default class Button extends Component { return ( { }, )} onClick={this.clickHandle} + style={innerStyle} {...rest} > {iconNode ? iconNode : null} diff --git a/src/checkbox/checkbox-group.tsx b/src/checkbox/checkbox-group.tsx index 42182da..6bd93ed 100644 --- a/src/checkbox/checkbox-group.tsx +++ b/src/checkbox/checkbox-group.tsx @@ -1,7 +1,8 @@ import { intersection, isObject, isString, isUndefined, toArray } from 'lodash'; import { bind, Component, signal, tag, VNode } from 'omi'; -import { getClassPrefix } from '../_util/classname.ts'; +import classname, { getClassPrefix } from '../_util/classname.ts'; +import { convertToLightDomNode } from '../_util/lightDom.ts'; import { StyledProps, TNode } from '../common'; import { CheckboxContextKey } from './checkbox'; import { @@ -182,22 +183,33 @@ export default class CheckboxGroup extends Component { const classPrefix = getClassPrefix(); let children = null; if (this.props.options?.length) { - children = this.optionList?.map((option, index) => ( - - )); + children = this.optionList?.map((option, index) => { + const { isLightDom, ...rest } = option; + const checkbox = ( + + ); + if (isLightDom) { + return convertToLightDomNode(checkbox); + } + return checkbox; + }); } else { this.innerOptionList.value = this.getOptionListBySlots(); children = this.props.children; } return ( -

+
{children}
); diff --git a/src/checkbox/index.ts b/src/checkbox/index.ts index e9630b3..180d342 100644 --- a/src/checkbox/index.ts +++ b/src/checkbox/index.ts @@ -10,4 +10,6 @@ export type CheckboxGroupProps = TdCheckboxGroupProps; export const Checkbox = _Checkbox; export const CheckboxGroup = _Group; +export * from './type'; + export default Checkbox; diff --git a/src/checkbox/type.ts b/src/checkbox/type.ts index 2561e48..9171bb0 100644 --- a/src/checkbox/type.ts +++ b/src/checkbox/type.ts @@ -42,6 +42,10 @@ export interface TdCheckboxProps { * 多选框的值 */ value?: string | number | boolean; + /** + * 是否去除 t-checkbox 的shadowdom,只在group中生效 + */ + isLightDom?: boolean; /** * 值变化时触发 */ diff --git a/src/common.ts b/src/common.ts index 79cfbc4..090e1c4 100644 --- a/src/common.ts +++ b/src/common.ts @@ -18,6 +18,10 @@ export type Styles = Record; export interface StyledProps { className?: string; style?: Styles; + // shadowDom内部根节点的class + innerClass?: string; + // shadowDom内部根节点的style + innerStyle?: Styles; } /** diff --git a/src/common/portal.tsx b/src/common/portal.tsx index 8163b3e..c4a87a8 100644 --- a/src/common/portal.tsx +++ b/src/common/portal.tsx @@ -68,6 +68,10 @@ export default class Portal extends Component { this.parentElement?.appendChild?.(this.container); } + uninstall(): void { + this.parentElement?.removeChild?.(this.container); + } + render() { const { children } = this.props; return render(children, this.container); diff --git a/src/index.ts b/src/index.ts index 2fe97ca..69a1e29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export * from './popup'; export * from './progress'; export * from './radio'; export * from './range-input'; +export * from './select-input'; export * from './skeleton'; export * from './slider'; export * from './space'; diff --git a/src/input/_example/base.tsx b/src/input/_example/base.tsx index 7598e58..e538ac3 100644 --- a/src/input/_example/base.tsx +++ b/src/input/_example/base.tsx @@ -12,6 +12,7 @@ export default class InputBase extends Component { return ( { this.value1 = value; diff --git a/src/input/input.tsx b/src/input/input.tsx index 58401ee..334f27e 100644 --- a/src/input/input.tsx +++ b/src/input/input.tsx @@ -43,6 +43,13 @@ const isFunction = (arg: unknown) => typeof arg === 'function'; @tag('t-input') export default class Input extends Component { + static css = [ + `:host { + width: 100%; + }; + `, + ]; + static defaultProps = { align: 'left', allowInputOverMax: false, @@ -127,7 +134,6 @@ export default class Input extends Component { allowInputOverMax, onValidate, }); - let { value: newStr } = e.currentTarget; if (this.composingRef.current) { this.composingValue = newStr; @@ -261,7 +267,7 @@ export default class Input extends Component { this.status = this.props.status; } - installed() { + ready() { this.renderType = this.props.type; const inputNode = this.inputRef.current; @@ -275,6 +281,8 @@ export default class Input extends Component { this.updateInputWidth(); }); + if (!inputNode) return; + inputNode.addEventListener('input', (e) => { if (this.composingRef.current) { this.composingValue = (e.currentTarget as HTMLInputElement)?.value || ''; @@ -318,12 +326,13 @@ export default class Input extends Component { render(props: OmiProps) { const { + innerClass, + innerStyle, autoWidth, placeholder, disabled, status, size, - className, prefixIcon, suffixIcon, clearable, @@ -342,11 +351,15 @@ export default class Input extends Component { keepWrapperWidth, showLimitNumber, allowInputOverMax, + inputClass, format, onValidate, ...restProps } = props; + delete restProps.className; + delete restProps.style; + const { limitNumber, tStatus } = useLengthLimit({ value: this.innerValue === undefined ? undefined : String(this.innerValue), status, @@ -363,7 +376,7 @@ export default class Input extends Component { 't', 'prefix', cloneElement(parseTNode(convertToLightDomNode(prefixIcon)) as VNode, { - className: `${classPrefix}-input__prefix`, + cls: `${classPrefix}-input__prefix`, style: { marginRight: '0px' }, }), ) @@ -375,7 +388,7 @@ export default class Input extends Component { e.preventDefault()} name={'close-circle-filled'} - className={classname( + cls={classname( `${classPrefix}-input__suffix-clear`, `${classPrefix}-input__suffix`, `${classPrefix}-input__suffix-icon`, @@ -391,7 +404,7 @@ export default class Input extends Component { e.preventDefault()} onClick={this.handlePasswordVisible} - className={classname( + cls={classname( `${classPrefix}-input__suffix-clear`, `${classPrefix}-input__suffix`, `${classPrefix}-input__suffix-icon`, @@ -404,7 +417,7 @@ export default class Input extends Component { e.preventDefault()} onClick={this.handlePasswordVisible} - className={classname( + cls={classname( `${classPrefix}-input__suffix-clear`, `${classPrefix}-input__suffix`, `${classPrefix}-input__suffix-icon`, @@ -456,7 +469,7 @@ export default class Input extends Component { ); const renderInputNode = (
{ { [`${classPrefix}-input--auto-width`]: autoWidth && !keepWrapperWidth, }, - className, + innerClass, )} ref={this.wrapperRef} part="wrap" @@ -508,6 +521,7 @@ export default class Input extends Component { restProps.onClick?.(e); }} {...restProps} + style={innerStyle} > {renderInputNode}
diff --git a/src/popup/_example/placement.tsx b/src/popup/_example/placement.tsx index a441efe..4303a1d 100644 --- a/src/popup/_example/placement.tsx +++ b/src/popup/_example/placement.tsx @@ -3,7 +3,7 @@ import 'tdesign-web-components/popup'; const styles = { container: { - margin: '0 auto', + margin: '20px auto', width: '500px', height: '260px', position: 'relative', @@ -74,50 +74,26 @@ const styles = { export default function Placement() { return (
- - top + + top - - top-left + + top-left - - top-right + + top-right - - bottom + + bottom - - bottom-left + + bottom-left - - bottom-right + + bottom-right - - left + + left - left-top + left-top - left-bottom + left-bottom - - right + + right - right-top + right-top - right-bottom + right-bottom
); diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index a8b84a1..a521ac3 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,14 +1,16 @@ import 'omi-transition'; import './popupTrigger'; +import '../common/portal'; import { createPopper } from '@popperjs/core'; import debounce from 'lodash/debounce'; -import { Component, createRef, OmiProps, tag } from 'omi'; +import { cloneElement, Component, createRef, OmiProps, tag, VNode } from 'omi'; import { getIEVersion } from '../_common/js/utils/helper'; import classname from '../_util/classname'; +import { getChildrenArray } from '../_util/component'; import { domContains } from '../_util/dom'; -import { StyledProps } from '../common'; +import { StyledProps, TNode } from '../common'; import { PopupVisibleChangeContext, TdPopupProps } from './type'; import { attachListeners, getPopperPlacement, triggers } from './utils'; @@ -40,7 +42,6 @@ export const PopupTypes = { onScroll: Function, onScrollToBottom: Function, onVisibleChange: Function, - strategy: String, expandAnimation: Boolean, updateScrollTop: Function, }; @@ -65,7 +66,6 @@ export default class Popup extends Component { placement: 'top', showArrow: true, trigger: 'hover', - strategy: 'fixed', }; triggerRef = createRef(); @@ -81,9 +81,13 @@ export default class Popup extends Component { hasDocumentEvent = false; visible = false; - // watch visible TODO: - hasTrigger = () => + // 防止多次触发显隐 + leaveFlag = false; + + isPopoverInDomTree = false; + + triggerType = () => triggers.reduce( (map, trigger) => ({ ...map, @@ -113,7 +117,15 @@ export default class Popup extends Component { if (typeof this.props.onVisibleChange === 'function') { this.props.onVisibleChange(visible, context); } + if (this.visible) { + this.isPopoverInDomTree = true; + } else if (this.props.destroyOnClose) { + this.isPopoverInDomTree = false; + } this.update(); + if (this.visible) { + this.addPopContentEvent(); + } }; handleOpen = (context: Pick) => { @@ -122,7 +134,7 @@ export default class Popup extends Component { () => { this.handlePopVisible(true, context); }, - this.hasTrigger().click ? 0 : this.normalizedDelay().open, + this.triggerType().click ? 0 : this.normalizedDelay().open, ); }; @@ -132,7 +144,7 @@ export default class Popup extends Component { () => { this.handlePopVisible(false, context); }, - this.hasTrigger().click ? 0 : this.normalizedDelay().close, + this.triggerType().click ? 0 : this.normalizedDelay().close, ); }; @@ -183,25 +195,31 @@ export default class Popup extends Component { } }; - updateTrigger = () => { - const trigger = attachListeners(this.rootElement); + addTriggerEvent = () => { + const triggerRef = this.triggerRef.current as HTMLElement; + + if (!triggerRef) return; + + const trigger = attachListeners(triggerRef); trigger.clean(); - const hasTrigger = this.hasTrigger(); - if (hasTrigger.hover) { + const triggerType = this.triggerType(); + if (triggerType.hover) { trigger.add('mouseenter', () => { + this.leaveFlag = false; this.handleOpen({ trigger: 'trigger-element-hover' }); }); trigger.add('mouseleave', () => { + this.leaveFlag = false; this.handleClose({ trigger: 'trigger-element-hover' }); }); - } else if (hasTrigger.focus) { + } else if (triggerType.focus) { trigger.add('focusin', () => this.handleOpen({ trigger: 'trigger-element-focus' })); trigger.add('focusout', () => this.handleClose({ trigger: 'trigger-element-blur' })); - } else if (hasTrigger.click) { + } else if (triggerType.click) { trigger.add('click', (e: MouseEvent) => { this.clickHandle(e); }); - } else if (hasTrigger['context-menu']) { + } else if (triggerType['context-menu']) { trigger.add('contextmenu', (e: MouseEvent) => { e.preventDefault(); e.button === 2 && this.handleToggle({ trigger: 'context-menu' }); @@ -209,10 +227,27 @@ export default class Popup extends Component { } }; - installed() { - this.updatePopper(); - this.updateTrigger(); - this.visible = this.props.visible; + addPopContentEvent() { + const popperEl = this.popperRef.current as HTMLElement; + if (!popperEl) return; + const popper = attachListeners(popperEl); + popper.clean(); + + const triggerType = this.triggerType(); + if (triggerType.hover) { + popper.add('mouseenter', () => { + if (!this.leaveFlag) { + clearTimeout(this.timeout); + this.handleOpen({ trigger: 'trigger-element-hover' }); + } + }); + + popper.add('mouseleave', () => { + this.leaveFlag = true; + clearTimeout(this.timeout); + this.handleClose({ trigger: 'trigger-element-hover' }); + }); + } } handleToggle = (context: PopupVisibleChangeContext) => { @@ -238,9 +273,9 @@ export default class Popup extends Component { } updatePopper = () => { - this.popper = createPopper(this.triggerRef.current as HTMLElement, this.popperRef.current as HTMLElement, { + createPopper(this.triggerRef.current as HTMLElement, this.popperRef.current as HTMLElement, { placement: getPopperPlacement(this.props.placement as PopupProps['placement']), - strategy: this.props.strategy, + ...(this.props?.popperOptions || {}), }); }; @@ -249,28 +284,40 @@ export default class Popup extends Component { handlePopVisible(visible, { trigger: 'document' }); }; + handleBeforeEnter = () => { + this.updatePopper(); + this.updatePopper(); + }; + beforeUpdate() { - // deal visible if (this.getVisible()) { if (this.popperRef.current) { const el = this.popperRef.current as HTMLElement; el.style.display = 'block'; } - this.updatePopper(); } else if (this.popperRef.current) { const el = this.popperRef.current as HTMLElement; el.style.display = 'none'; } } - handleBeforeEnter = () => { - this.updatePopper(); - }; - install(): void { window.addEventListener('resize', this.updatePopper); } + installed() { + this.updatePopper(); + this.addTriggerEvent(); + + this.visible = this.props.visible; + // 初始化就显示时 + if (this.visible) { + this.isPopoverInDomTree = true; + this.update(); + this.addPopContentEvent(); + } + } + uninstall(): void { window.removeEventListener('resize', this.updatePopper); } @@ -287,34 +334,44 @@ export default class Popup extends Component { props.overlayInnerClassName, ); - const trigger = props.triggerElement ? props.triggerElement : this.props.children; + const trigger = getChildrenArray(props.triggerElement ? props.triggerElement : this.props.children); + + const children = trigger.map((child: TNode) => { + // 对 t-button 做特殊处理 + if (typeof child === 'object' && (child as any).nodeName === 't-button') { + const oldClick = (child as VNode).attributes?.onClick; + return cloneElement(child as VNode, { + onClick: (e) => { + if (oldClick) oldClick(e); + this.clickHandle(e.detail.e); + }, + }); + } + return child; + }); return ( - - { - if (e?.detail?.context?.nodeName === 'T-BUTTON') { - this.clickHandle(e.detail.e); - } - }} - > - {trigger} - - {this.getVisible() || !props.destroyOnClose ? ( -
(this.contentClicked = true)} - > - {(this.getVisible() || this.popperRef.current) && ( + <> + {children.length > 1 ? ( + + {children} + + ) : ( + cloneElement(children[0] as VNode, { ref: this.triggerRef }) + )} + {this.isPopoverInDomTree ? ( + +
(this.contentClicked = true)} + >
{
) : null}
- )} -
+
+
) : null} - + ); } } diff --git a/src/select-input/README.md b/src/select-input/README.md new file mode 100644 index 0000000..b7fabca --- /dev/null +++ b/src/select-input/README.md @@ -0,0 +1,143 @@ +--- +title: SelectInput 筛选器输入框 +description: 定义:筛选器通用输入框, +isComponent: true +usage: { title: '', description: '' } +spline: data +--- + +### 筛选器输入框 + +统一筛选器逻辑包含:输入框、下拉框、无边框模式等,可以使用筛选器输入框定制复杂的筛选器。 + +基于 `TagInput` `Input` `Popup` 等组件开发,支持这些组件的全部特性。将主要应用于 `Select` `Cascader` `TreeSelect` `DatePicker` `TimePicker` 等筛选器组件。 + +### 单选筛选器输入框 + +可使用 `SelectInput` 自由定制任何风格的单选选择器。 + +{{ single }} + +### 多选筛选器输入框 + +可使用 `SelectInput` 自由定制任何风格的多选选择器。 + +{{ multiple }} + +### 自动填充筛选器 + +可使用 `SelectInput` 自由定制任何风格的自动填充筛选器。 + +{{ autocomplete }} + +### 有前置或后置内容的输入框 + +- 前置内容使用 `label` 自定义。 +- 后置内容使用 `suffix` 自定义。 +- 前置图标使用 `prefixIcon` 自定义。 +- 后置图标使用 `suffixIcon` 自定义。 + +{{ label-suffix }} + +### 不同状态的筛选器输入框 + +使用 `status` 和 `tips` 控制状态和提示文案。 + +{{ status }} + +### 可调整下拉框宽度的筛选器输入框 + +下拉框宽度规则:下拉框宽度默认和触发元素宽度保持同宽,如果下拉框宽度超出输入框组件会自动撑开下拉框宽度,但最大宽度不超过 `1000px`。也可以通过 `popupProps.overlayInnerStyle.width` 自由设置下拉框宽度。`popupProps.overlayInnerStyle` 类型为函数时,可以更灵活地动态控制下拉框宽度。 + +{{ width }} + +### 选中项数量超出的输入框 + +使用 `excessTagsDisplayType` 控制标签超出时的呈现方式:横向滚动显示和换行显示,默认为换行显示。 + +{{ excess-tags-display-type }} + + +### 可折叠选中项的筛选器输入框 + +选中项数量超过 `minCollapsedNum` 时会被折叠,可使用 `collapsedItems` 自定义折叠选项中的呈现方式。 + +{{ collapsed-items }} + +### 可自定义选中项的筛选器输入框 + +使用 `valueDisplay` 或者 `tag` 自定义选中项。 + +{{ custom-tag }} + +### 无边框模式的单选筛选器 + +`borderless` 用于控制是否呈现为无边框模式。 + +{{ borderless }} + +### 无边框模式的多选筛选器 + +{{ borderless-multiple }} + +### 自动宽度的单选筛选器 + +{{ autowidth }} + + +### 自动宽度的多选筛选器 + +{{ autowidth-multiple }} + + +## API + +### SelectInput Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +allowInput | Boolean | false | 是否允许输入 | N +autoWidth | Boolean | false | 宽度随内容自适应 | N +autofocus | Boolean | false | 自动聚焦 | N +borderless | Boolean | false | 无边框模式 | N +clearable | Boolean | false | 是否可清空 | N +collapsedItems | TElement | - | 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 `collapsedItems` 自定义。`value` 表示所有标签值,`collapsedSelectedItems` 表示折叠标签值,`count` 表示折叠的数量,`onClose` 表示移除标签的事件回调。TS 类型:`TNode<{ value: SelectInputValue; collapsedSelectedItems: SelectInputValue; count: number; onClose: (context: { index: number, e?: MouseEvent }) => void }>`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +disabled | Boolean | - | 是否禁用 | N +inputProps | Object | - | 透传 Input 输入框组件全部属性。TS 类型:`InputProps`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +inputValue | String / Number | - | 输入框的值。TS 类型:`string` | N +defaultInputValue | String / Number | - | 输入框的值。非受控属性。TS 类型:`string` | N +keys | Object | - | 定义字段别名,示例:`{ label: 'text', value: 'id', children: 'list' }`。TS 类型:`SelectInputKeys` `interface SelectInputKeys { label?: string; value?: string; children?: string }`。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +label | TNode | - | 左侧文本。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +loading | Boolean | false | 是否处于加载状态 | N +minCollapsedNum | Number | 0 | 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 | N +multiple | Boolean | false | 是否为多选模式,默认为单选 | N +panel | TNode | - | 下拉框内容,可完全自定义。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +placeholder | String | - | 占位符 | N +popupProps | Object | - | 透传 Popup 浮层组件全部属性。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +popupVisible | Boolean | - | 是否显示下拉框 | N +defaultPopupVisible | Boolean | - | 是否显示下拉框。非受控属性 | N +prefixIcon | TElement | - | 组件前置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +readonly | Boolean | false | 只读状态,值为真会隐藏输入框,且无法打开下拉框 | N +reserveKeyword | Boolean | false | 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 | N +size | String | medium | 组件尺寸。可选项:small/medium/large。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +status | String | default | 输入框状态。可选项:default/success/warning/error | N +suffix | TNode | - | 后置图标前的后置内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +suffixIcon | TElement | - | 组件后置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +tag | TNode | - | 多选场景下,自定义选中标签的内部内容。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签。TS 类型:`string \| TNode<{ value: string \| number }>`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +tagInputProps | Object | - | 透传 TagInput 组件全部属性。TS 类型:`TagInputProps`,[TagInput API Documents](./tag-input?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +tagProps | Object | - | 透传 Tag 标签组件全部属性。TS 类型:`TagProps`,[Tag API Documents](./tag?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +tips | TNode | - | 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +value | String / Number / Boolean / Object / Array / Date | undefined | 全部标签值。值为数组表示多个标签,值为非数组表示单个数值。TS 类型:`SelectInputValue` `type SelectInputValue = string \| number \| boolean \| Date \| Object \| Array \| Array`。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts) | N +valueDisplay | TNode | - | 自定义值呈现的全部内容,参数为所有标签的值。TS 类型:`string \| TNode<{ value: TagInputValue; onClose: (index: number, item?: any) => void }>`。[通用类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/common.ts) | N +onBlur | Function | | TS 类型:`(value: SelectInputValue, context: SelectInputBlurContext) => void`
失去焦点时触发,`context.inputValue` 表示输入框的值;`context.tagInputValue` 表示标签输入框的值。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts)。
`type SelectInputBlurContext = PopupVisibleChangeContext & { inputValue: string; tagInputValue?: TagInputValue; }`
| N +onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
清空按钮点击时触发 | N +onEnter | Function | | TS 类型:`(value: SelectInputValue, context: { e: KeyboardEvent; inputValue: string; tagInputValue?: TagInputValue }) => void`
按键按下 Enter 时触发 | N +onFocus | Function | | TS 类型:`(value: SelectInputValue, context: SelectInputFocusContext) => void`
聚焦时触发。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts)。
`interface SelectInputFocusContext { inputValue: string; tagInputValue?: TagInputValue; e: FocusEvent }`
| N +onInputChange | Function | | TS 类型:`(value: string, context?: SelectInputValueChangeContext) => void`
输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发等。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts)。
`interface SelectInputValueChangeContext { e?: Event \| InputEvent \| MouseEvent \| FocusEvent \| KeyboardEvent \| CompositionEvent; trigger: 'input' \| 'clear' \| 'blur' \| 'focus' \| 'initial' \| 'change' }`
| N +onMouseenter | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
进入输入框时触发 | N +onMouseleave | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
离开输入框时触发 | N +onPaste | Function | | TS 类型:`(context: { e: ClipboardEvent; pasteValue: string }) => void`
粘贴事件,`pasteValue` 表示粘贴板的内容 | N +onPopupVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVisibleChangeContext) => void`
下拉框显示或隐藏时触发。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts)。
`import { PopupVisibleChangeContext } from '@Popup'`
| N +onTagChange | Function | | TS 类型:`(value: TagInputValue, context: SelectInputChangeContext) => void`
值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数。[详细类型定义](https://github.com/TDesignOteam/tdesign-web-components/tree/main/src/select-input/type.ts)。
`type SelectInputChangeContext = TagInputChangeContext`
| N diff --git a/src/select-input/SelectInput.tsx b/src/select-input/SelectInput.tsx new file mode 100644 index 0000000..144e9e1 --- /dev/null +++ b/src/select-input/SelectInput.tsx @@ -0,0 +1,225 @@ +import '../loading'; +import './SelectInputMultiple'; +import './SelectInputSingle'; +import '../popup'; + +import classNames from 'classnames'; +import { pick } from 'lodash'; +import { Component, createRef, OmiProps, tag } from 'omi'; + +import { getClassPrefix } from '../_util/classname'; +import { StyledProps } from '../common'; +import { PopupVisibleChangeContext } from '../popup'; +import { selectInputDefaultProps } from './defaultProps'; +import { SelectInputCommonProperties } from './interface'; +import { TdSelectInputProps } from './type'; +import useOverlayInnerStyle from './useOverlayInnerStyle'; + +export interface SelectInputProps extends TdSelectInputProps, StyledProps { + updateScrollTop?: (content: HTMLDivElement) => void; +} + +// single 和 multiple 共有特性 +const COMMON_PROPERTIES = [ + 'status', + 'clearable', + 'disabled', + 'label', + 'placeholder', + 'readonly', + 'suffix', + 'suffixIcon', + 'onPaste', + 'onEnter', + 'onMouseenter', + 'onMouseleave', + 'size', + 'prefixIcon', +]; + +const classPrefix = getClassPrefix(); + +@tag('t-select-input') +class SelectInput extends Component { + static css = [ + `.${classPrefix}-select-input > ${classPrefix}-popup { + display: inline-flex; + width: 100%; + };`, + ]; + + static defaultProps = selectInputDefaultProps; + + static propTypes = { + allowInput: Boolean, + autoWidth: Boolean, + autofocus: Boolean, + borderless: Boolean, + clearable: Boolean, + collapsedItems: [Function, Object, String, Number], + disabled: Boolean, + inputProps: Object, + inputValue: String, + defaultInputValue: String, + keys: Object, + label: [Function, Object, String, Number], + loading: Boolean, + minCollapsedNum: Number, + multiple: Boolean, + panel: [Function, Object, String, Number], + placeholder: String, + popupProps: Object, + popupVisible: Boolean, + defaultPopupVisible: Boolean, + prefixIcon: [Function, Object, String, Number], + readonly: Boolean, + reserveKeyword: Boolean, + size: String, + status: String, + suffix: [Function, Object, String, Number], + suffixIcon: [Function, Object, String, Number], + tag: [String, Function, Object, Number], + tagInputProps: Object, + tagProps: Object, + tips: [Function, Object, String, Number], + value: [String, Number, Boolean, Object, Array], + valueDisplay: [String, Function, Object, Number], + onBlur: Function, + onClear: Function, + onEnter: Function, + onFocus: Function, + onInputChange: Function, + onMouseenter: Function, + onMouseleave: Function, + onPaste: Function, + onPopupVisibleChange: Function, + onTagChange: Function, + }; + + selectInputRef = createRef(); + + selectInputWrapRef = createRef(); + + classPrefix = classPrefix; + + commonInputProps: SelectInputCommonProperties; + + tOverlayInnerStyle; + + innerPopupVisible; + + onInnerPopupVisibleChange; + + multipleInputValue; + + singleInputValue; + + install(): void { + const { loading, suffixIcon, multiple } = this.props; + this.commonInputProps = { + ...pick(this.props, COMMON_PROPERTIES), + suffixIcon: loading ? : suffixIcon, + }; + + const { innerPopupVisible, tOverlayInnerStyle, onInnerPopupVisibleChange } = useOverlayInnerStyle( + this.props, + { + afterHidePopup: this.onInnerBlur.bind(this), + }, + this, + ); + this.tOverlayInnerStyle = tOverlayInnerStyle; + this.innerPopupVisible = innerPopupVisible; + this.onInnerPopupVisibleChange = onInnerPopupVisibleChange; + + if (multiple) { + this.multipleInputValue = this.props.inputValue || this.props.defaultInputValue; + } else { + this.singleInputValue = this.props.inputValue || this.props.defaultInputValue; + } + } + + updateValue = (val, key: 'multipleInputValue' | 'singleInputValue') => { + this[key] = val; + }; + + onInnerBlur(ctx: PopupVisibleChangeContext) { + const inputValue = this.props.multiple ? this.multipleInputValue : this.singleInputValue; + const params: Parameters[1] = { e: ctx.e, inputValue }; + this.props.onBlur?.(this.props.value, params); + } + + render(props: SelectInputProps | OmiProps) { + const { multiple, value, popupVisible, popupProps, borderless, disabled } = props; + + // 浮层显示的受控与非受控 + const visibleProps = { visible: popupVisible ?? this.innerPopupVisible }; + + const popupClasses = classNames([ + !props.tips ? props.innerClass : '', + `${this.classPrefix}-select-input`, + { + [`${this.classPrefix}-select-input--borderless`]: borderless, + [`${this.classPrefix}-select-input--multiple`]: multiple, + [`${this.classPrefix}-select-input--popup-visible`]: popupVisible ?? this.innerPopupVisible, + [`${this.classPrefix}-select-input--empty`]: value instanceof Array ? !value.length : !value, + }, + ]); + + const mainContent = ( +
+ + {multiple ? ( + + ) : ( + + )} + +
+ ); + + if (!props.tips) { + return mainContent; + } + + return ( +
+ {mainContent} + {props.tips && ( +
+ {props.tips} +
+ )} +
+ ); + } +} + +export default SelectInput; diff --git a/src/select-input/SelectInputMultiple.tsx b/src/select-input/SelectInputMultiple.tsx new file mode 100644 index 0000000..06935d6 --- /dev/null +++ b/src/select-input/SelectInputMultiple.tsx @@ -0,0 +1,134 @@ +import '../tag-input'; + +import classNames from 'classnames'; +import isObject from 'lodash/isObject'; +import { Component, createRef, tag } from 'omi'; + +import { getClassPrefix } from '../_util/classname'; +import { TagInputValue } from '../tag-input'; +import { SelectInputCommonProperties } from './interface'; +import { SelectInputChangeContext, SelectInputKeys, SelectInputValue, TdSelectInputProps } from './type'; + +export interface RenderSelectMultipleParams { + commonInputProps: SelectInputCommonProperties; + onInnerClear: (context: { e: MouseEvent }) => void; + popupVisible: boolean; + allowInput: boolean; +} + +const DEFAULT_KEYS = { + label: 'label', + key: 'key', + children: 'children', +}; + +const classPrefix = getClassPrefix(); + +const autoWidthCss = ` +.${classPrefix}-input--auto-width.${classPrefix}-tag-input__with-suffix-icon.${classPrefix}-tag-input--with-tag .${classPrefix}-input { + padding-right: var(--td-comp-paddingLR-xl); +}; +`; +@tag('t-select-input-multiple') +export default class SelectInputMultiple extends Component< + TdSelectInputProps & { onUpdateValue: (val: TdSelectInputProps['inputValue'], key: string) => void } +> { + static css = [ + `:host { + width: 100%; + }; + `, + ]; + + classPrefix = classPrefix; + + tagInputRef = createRef(); + + tInputValue; + + install(): void { + this.tInputValue = this.props.inputValue || this.props.defaultInputValue; + } + + onTagInputChange = (val: TagInputValue, context: SelectInputChangeContext) => { + // 避免触发浮层的显示或隐藏 + if (context.trigger === 'tag-remove') { + context.e?.stopPropagation(); + } + this.props.onTagChange?.(val, context); + }; + + onInnerClear = (context: { e: MouseEvent }) => { + context?.e?.stopPropagation(); + this.props.onClear?.(context); + this.tInputValue = ''; + this.props?.onInputChange?.('', { trigger: 'clear' }); + this.props.onUpdateValue('', 'multipleInputValue'); + }; + + receiveProps(newProps) { + if (newProps.inputValue !== this.tInputValue) { + this.tInputValue = newProps.inputValue; + } + } + + render(props) { + const { value, popupVisible, commonInputProps, allowInput, borderless, autoWidth } = props; + const iKeys: SelectInputKeys = { ...DEFAULT_KEYS, ...props.keys }; + + const getTags = () => { + if (!(value instanceof Array)) { + return isObject(value) ? [value[iKeys.label]] : [value]; + } + return value.map((item: SelectInputValue) => (isObject(item) ? item[iKeys.label] : item)); + }; + const tags = getTags(); + + const tPlaceholder = !tags || !tags.length ? props.placeholder : ''; + + const inputCss = (props?.inputProps?.css || '') + (autoWidth ? autoWidthCss : ''); + + return ( + { + // 筛选器统一特性:筛选器按下回车时不清空输入框 + if (context?.trigger === 'enter' || context?.trigger === 'blur') return; + this.tInputValue = val; + props?.onInputChange?.(val, { trigger: context.trigger, e: context.e }); + this.props.onUpdateValue(val, 'multipleInputValue'); + }} + tagProps={props.tagProps} + onClear={this.onInnerClear} + // [Important Info]: SelectInput.blur is not equal to TagInput, example: click popup panel + onFocus={(val, context) => { + props.onFocus?.(props.value, { ...context, tagInputValue: val }); + }} + onBlur={!props.panel ? props.onBlur : null} + style={{ width: '100%', display: 'inline-flex' }} + borderless={borderless} + {...props.tagInputProps} + inputProps={{ + ...props.inputProps, + readonly: !props.allowInput || props.readonly, + inputClass: classNames(props.tagInputProps?.className, { + [`${this.classPrefix}-input--focused`]: popupVisible, + [`${this.classPrefix}-is-focused`]: popupVisible, + }), + css: inputCss, + }} + /> + ); + } +} diff --git a/src/select-input/SelectInputSingle.tsx b/src/select-input/SelectInputSingle.tsx new file mode 100644 index 0000000..7956af6 --- /dev/null +++ b/src/select-input/SelectInputSingle.tsx @@ -0,0 +1,113 @@ +import '../input'; + +import classNames from 'classnames'; +import isObject from 'lodash/isObject'; +import { Component, createRef, tag } from 'omi'; + +import { getClassPrefix } from '../_util/classname'; +// import useControlled from '../_util/useControlled'; +import { TdInputProps } from '../input'; +import { TdSelectInputProps } from './type'; + +export interface RenderSelectSingleInputParams { + tPlaceholder: string; +} + +const DEFAULT_KEYS: TdSelectInputProps['keys'] = { + label: 'label', + value: 'value', +}; + +function getInputValue(value: TdSelectInputProps['value'], keys: TdSelectInputProps['keys']) { + const iKeys = keys || DEFAULT_KEYS; + return isObject(value) ? value[iKeys.label] : value; +} + +@tag('t-select-input-single') +export default class SingleSelectInput extends Component< + TdSelectInputProps & { onUpdateValue: (val: TdSelectInputProps['inputValue'], key: string) => void } +> { + static css = [ + `:host { + width: 100%; + }; + `, + ]; + + classPrefix = getClassPrefix(); + + inputRef = createRef(); + + inputValue; + + setInputValue; + + install(): void { + this.inputValue = this.props.inputValue || this.props.defaultInputValue; + } + + onInnerClear = (context: { e: MouseEvent }) => { + context?.e?.stopPropagation(); + this.props.onClear?.(context); + this.inputValue = ''; + this.props?.onInputChange?.('', { trigger: 'clear' }); + this.props.onUpdateValue('', 'singleInputValue'); + }; + + onInnerInputChange: TdInputProps['onChange'] = (value, context) => { + if (this.props.allowInput) { + this.inputValue = value; + this.props?.onInputChange?.(value, { ...context, trigger: 'input' }); + this.props.onUpdateValue(value, 'singleInputValue'); + } + }; + + handleEmptyPanelBlur = (value: string, { e }: { e: FocusEvent }) => { + this.props.onBlur?.(value, { e, inputValue: value }); + }; + + render(props) { + const { value, keys, commonInputProps, popupVisible, borderless } = props; + + // 单选,值的呈现方式 + const singleValueDisplay = !props.multiple ? props.valueDisplay : null; + + const displayedValue = popupVisible && props.allowInput ? this.inputValue : getInputValue(value, keys); + + return ( + + {props.label} + {singleValueDisplay} + + } + onChange={this.onInnerInputChange} + readonly={!props.allowInput} + onClear={this.onInnerClear} + // [Important Info]: SelectInput.blur is not equal to Input, example: click popup panel + onFocus={(val, context) => { + props.onFocus?.(value, { ...context, inputValue: val }); + // focus might not need to change input value. it will caught some curious errors in tree-select + // !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' }); + }} + onEnter={(val, context) => { + props.onEnter?.(value, { ...context, inputValue: val }); + }} + // onBlur need to triggered by input when popup panel is null + onBlur={!props.panel ? this.handleEmptyPanelBlur : null} + borderless={borderless} + {...props.inputProps} + inputClass={classNames(props.inputProps?.className, { + [`${this.classPrefix}-input--focused`]: popupVisible, + [`${this.classPrefix}-is-focused`]: popupVisible, + })} + /> + ); + } +} diff --git a/src/select-input/_example/autocomplete.tsx b/src/select-input/_example/autocomplete.tsx new file mode 100644 index 0000000..29f1543 --- /dev/null +++ b/src/select-input/_example/autocomplete.tsx @@ -0,0 +1,93 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/search'; + +import { Component, signal } from 'omi'; + +const classStyles = ` + +`; + +const OPTIONS = ['Student A', 'Student B', 'Student C', 'Student D', 'Student E', 'Student F']; + +export default class SelectInputAutocomplete extends Component { + popupVisible = signal(false); + + selectValue = ''; + + options = OPTIONS; + + onOptionClick = (item: string) => { + this.selectValue = item; + + // this.update(); + this.popupVisible.value = false; + }; + + onInputChange = (keyword: string) => { + this.selectValue = keyword; + const options = new Array(5).fill(null).map((t, index) => `${keyword} Student ${index}`); + this.options = options; + this.update(); + }; + + onPopupVisibleChange = (val: boolean) => { + this.popupVisible.value = val; + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + // 如果需要输入框宽度自适应,可以使用 autoWidth + render() { + return ( +
+ + {this.options.map((item) => ( +
  • this.onOptionClick(item)}> + {item} +
  • + ))} + + } + suffixIcon={} + /> +
    + ); + } +} diff --git a/src/select-input/_example/autowidth-multiple.tsx b/src/select-input/_example/autowidth-multiple.tsx new file mode 100644 index 0000000..4636cc5 --- /dev/null +++ b/src/select-input/_example/autowidth-multiple.tsx @@ -0,0 +1,133 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; +import 'tdesign-web-components/checkbox'; + +import { Component, signal } from 'omi'; +import { CheckboxGroupProps, CheckboxOptionObj } from 'tdesign-web-components/checkbox'; +import { TagInputChangeContext, TagInputValue } from 'tdesign-web-components/tag-input'; + +const classStyles = ` +.tdesign-demo__panel-options-autowidth-multiple { + width: 100%; + padding: 2px 0; + margin: 0 -2px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.tdesign-demo__panel-options-autowidth-multiple .t-checkbox { + display: flex; + border-radius: 3px; + line-height: 22px; + cursor: pointer; + padding: 3px 8px; + color: var(--td-text-color-primary); + transition: background-color 0.2s linear; + white-space: nowrap; + word-wrap: normal; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; +} + +.tdesign-demo__panel-options-autowidth-multiple .t-checkbox:hover { + background-color: var(--td-bg-color-container-hover); +} +`; + +const OPTIONS: CheckboxOptionObj[] = [ + // 全选 + { label: 'Check All', checkAll: true }, + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputMultiple extends Component { + options = signal([...OPTIONS]); + + value = signal([ + { label: 'Vue', value: 1 }, + { label: 'React', value: 2 }, + { label: 'Miniprogram', value: 3 }, + ]); + + getCheckboxValue = () => { + const arr = []; + const list = this.value.value; + // 此处不使用 forEach,减少函数迭代 + for (let i = 0, len = list.length; i < len; i++) { + list[i].value && arr.push(list[i].value); + } + return arr; + }; + + // 直接 checkboxgroup 组件渲染输出下拉选项,自定义处理可以避免顺序和 tagChange 冲突 + onCheckedChange: CheckboxGroupProps['onChange'] = (val, { current, type }) => { + // current 不存在,则表示操作全选 + if (!current) { + const newValue = type === 'check' ? this.options.value.slice(1) : []; + this.value.value = newValue; + return; + } + // 普通操作 + if (type === 'check') { + const option = this.options.value.find((t) => t.value === current); + this.value.value = this.value.value.concat(option); + } else { + const newValue = this.value.value.filter((v) => v.value !== current); + this.value.value = newValue; + } + }; + + // 可以根据触发来源,自由定制标签变化时的筛选器行为 + onTagChange = (currentTags: TagInputValue, context: TagInputChangeContext) => { + const { trigger, index, item } = context; + if (trigger === 'clear') { + this.value.value = []; + } + if (['tag-remove', 'backspace'].includes(trigger)) { + const newValue = [...this.value.value]; + newValue.splice(index, 1); + this.value.value = newValue; + } + // 如果允许创建新条目 + if (trigger === 'enter') { + const current = { label: item, value: item }; + const newValue = [...this.value.value]; + this.value.value = newValue.concat(current); + this.options.value = this.options.value.concat(current); + } + }; + + render() { + return ( +
    + } + onTagChange={this.onTagChange} + panel={ + + } + /> +
    + ); + } +} diff --git a/src/select-input/_example/autowidth.tsx b/src/select-input/_example/autowidth.tsx new file mode 100644 index 0000000..2e37810 --- /dev/null +++ b/src/select-input/_example/autowidth.tsx @@ -0,0 +1,100 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; +import { SelectInputValueChangeContext } from 'tdesign-web-components/select-input'; + +const classStyles = ` + +`; + +const OPTIONS = [ + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputAutocomplete extends Component { + popupVisible = signal(false); + + selectValue = signal({ label: 'tdesign-vue', value: 1 }); + + onOptionClick = (item: { label: string; value: number }) => { + this.selectValue.value = item; + this.popupVisible.value = false; + }; + + onClear = () => { + this.selectValue.value = undefined; + }; + + onPopupVisibleChange = (val: boolean) => { + this.popupVisible.value = val; + }; + + onInputChange = (val: string, context: SelectInputValueChangeContext) => { + // 过滤功能 + console.log(val, context); + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + // 如果需要输入框宽度自适应,可以使用 autoWidth + return ( + } + panel={ +
      + {OPTIONS.map((item) => ( +
    • this.onOptionClick(item)}> + {item.label} +
    • + ))} +
    + } + /> + ); + } +} diff --git a/src/select-input/_example/borderless-multiple.tsx b/src/select-input/_example/borderless-multiple.tsx new file mode 100644 index 0000000..f0d642b --- /dev/null +++ b/src/select-input/_example/borderless-multiple.tsx @@ -0,0 +1,137 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; +import 'tdesign-web-components/checkbox'; + +import { Component, signal } from 'omi'; +import { CheckboxGroupProps, CheckboxOptionObj } from 'tdesign-web-components/checkbox'; +import { TagInputChangeContext, TagInputValue } from 'tdesign-web-components/tag-input'; + +const classStyles = ` +.tdesign-demo__panel-options-borderless-multiple { + width: 100%; + padding: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.tdesign-demo__panel-options-borderless-multiple .t-checkbox { + display: flex; + border-radius: 3px; + line-height: 22px; + cursor: pointer; + padding: 3px 8px; + color: var(--td-text-color-primary); + transition: background-color 0.2s linear; + white-space: nowrap; + word-wrap: normal; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; +} + +.tdesign-demo__panel-options-borderless-multiple .t-checkbox:hover { + background-color: var(--td-bg-color-container-hover); +} +`; + +const OPTIONS: CheckboxOptionObj[] = [ + // 全选 + { label: 'Check All', checkAll: true }, + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputMultiple extends Component { + options = signal([...OPTIONS]); + + value = signal([ + { label: 'Vue', value: 1 }, + { label: 'React', value: 2 }, + { label: 'Miniprogram', value: 3 }, + ]); + + getCheckboxValue = () => { + const arr = []; + const list = this.value.value; + // 此处不使用 forEach,减少函数迭代 + for (let i = 0, len = list.length; i < len; i++) { + list[i].value && arr.push(list[i].value); + } + return arr; + }; + + // 直接 checkboxgroup 组件渲染输出下拉选项,自定义处理可以避免顺序和 tagChange 冲突 + onCheckedChange: CheckboxGroupProps['onChange'] = (val, { current, type }) => { + // current 不存在,则表示操作全选 + if (!current) { + const newValue = type === 'check' ? this.options.value.slice(1) : []; + this.value.value = newValue; + return; + } + // 普通操作 + if (type === 'check') { + const option = this.options.value.find((t) => t.value === current); + this.value.value = this.value.value.concat(option); + } else { + const newValue = this.value.value.filter((v) => v.value !== current); + this.value.value = newValue; + } + }; + + // 可以根据触发来源,自由定制标签变化时的筛选器行为 + onTagChange = (currentTags: TagInputValue, context: TagInputChangeContext) => { + const { trigger, index, item } = context; + if (trigger === 'clear') { + this.value.value = []; + } + if (['tag-remove', 'backspace'].includes(trigger)) { + const newValue = [...this.value.value]; + newValue.splice(index, 1); + this.value.value = newValue; + } + // 如果允许创建新条目 + if (trigger === 'enter') { + const current = { label: item, value: item }; + const newValue = [...this.value.value]; + this.value.value = newValue.concat(current); + this.options.value = this.options.value.concat(current); + } + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + } + onTagChange={this.onTagChange} + panel={ + + } + /> +
    + ); + } +} diff --git a/src/select-input/_example/borderless.tsx b/src/select-input/_example/borderless.tsx new file mode 100644 index 0000000..847597b --- /dev/null +++ b/src/select-input/_example/borderless.tsx @@ -0,0 +1,95 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; +import { SelectInputProps } from 'tdesign-web-components/select-input'; + +const classStyles = ` + +`; + +const OPTIONS = [ + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputSingle extends Component { + selectValue = signal({ label: 'tdesign-vue', value: 1 }); + + popupVisible = signal(false); + + onOptionClick = (item: { label: string; value: number }) => { + this.selectValue.value = item; + // 选中后立即关闭浮层 + this.popupVisible.value = false; + }; + + onClear = () => { + this.selectValue.value = undefined; + }; + + onPopupVisibleChange: SelectInputProps['onPopupVisibleChange'] = (val, context) => { + console.log(context); + this.popupVisible.value = val; + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + {/* */} + + {OPTIONS.map((item) => ( +
  • this.onOptionClick(item)}> + {item.label} +
  • + ))} + + } + suffixIcon={} + >
    +
    + ); + } +} diff --git a/src/select-input/_example/collapsed-items.tsx b/src/select-input/_example/collapsed-items.tsx new file mode 100644 index 0000000..818e160 --- /dev/null +++ b/src/select-input/_example/collapsed-items.tsx @@ -0,0 +1,155 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-web-components/checkbox'; +import 'tdesign-web-components/space'; +import 'tdesign-web-components/tag'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; +import { CheckboxGroupProps, CheckboxOptionObj } from 'tdesign-web-components/checkbox'; +import { SelectInputProps } from 'tdesign-web-components/select-input'; +import { TagInputChangeContext, TagInputValue } from 'tdesign-web-components/tag-input'; + +const classStyles = ` + +`; + +const OPTIONS: CheckboxOptionObj[] = [ + // 全选 + { label: 'Check All', checkAll: true }, + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputCollapsedItems extends Component { + options = signal([...OPTIONS]); + + value = signal(OPTIONS.slice(1)); + + getCheckboxValue = () => { + const arr = []; + const list = this.value.value; + // 此处不使用 forEach,减少函数迭代 + for (let i = 0, len = list.length; i < len; i++) { + list[i].value && arr.push(list[i].value); + } + return arr; + }; + + // 直接 checkboxgroup 组件渲染输出下拉选项,自定义处理可以避免顺序和 tagChange 冲突 + onCheckedChange: CheckboxGroupProps['onChange'] = (val, { current, type }) => { + // current 不存在,则表示操作全选 + if (!current) { + const newValue = type === 'check' ? this.options.value.slice(1) : []; + this.value.value = newValue; + return; + } + // 普通操作 + if (type === 'check') { + const option = this.options.value.find((t) => t.value === current); + this.value.value = this.value.value.concat(option); + } else { + const newValue = this.value.value.filter((v) => v.value !== current); + this.value.value = newValue; + } + }; + + // 可以根据触发来源,自由定制标签变化时的筛选器行为 + onTagChange = (currentTags: TagInputValue, context: TagInputChangeContext) => { + const { trigger, index, item } = context; + if (trigger === 'clear') { + this.value.value = []; + } + if (['tag-remove', 'backspace'].includes(trigger)) { + const newValue = [...this.value.value]; + newValue.splice(index, 1); + this.value.value = newValue; + } + // 如果允许创建新条目 + if (trigger === 'enter') { + const current = { label: item, value: item }; + const newValue = [...this.value.value]; + this.value.value = newValue.concat(current); + this.options.value = this.options.value.concat(current); + } + }; + + CheckboxPanel = ( + + ); + + handleCollapsedItems: SelectInputProps['collapsedItems'] = ({ collapsedSelectedItems }) => { + if (Array.isArray(collapsedSelectedItems)) { + return More(+{collapsedSelectedItems.length}); + } + return null; + }; + + install(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( + + {/* */} + } + clearable + multiple + onTagChange={this.onTagChange} + /> +

    + {/* 使用 collapsedItems 自定义折叠标签 */} + } + collapsedItems={this.handleCollapsedItems} + clearable + multiple + onTagChange={this.onTagChange} + /> +
    + ); + } +} diff --git a/src/select-input/_example/custom-tag.tsx b/src/select-input/_example/custom-tag.tsx new file mode 100644 index 0000000..13375b7 --- /dev/null +++ b/src/select-input/_example/custom-tag.tsx @@ -0,0 +1,165 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/control-platform'; +import 'tdesign-web-components/tag'; + +import { Component, signal } from 'omi'; + +const classStyles = ` + +`; + +const inputStyles = ` + +`; + +const OPTIONS = [ + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputCustomTag extends Component { + selectValue1 = signal({ label: 'tdesign-vue', value: 1 }); + + selectValue2 = signal(['tdesign-vue', 'tdesign-react']); + + selectValue3 = signal(['tdesign-vue', 'tdesign-react', 'tdesign-mobile-vue']); + + onOptionClick = (item: { label: string; value: number }) => { + this.selectValue1.value = item; + }; + + onClear = () => { + this.selectValue1.value = undefined; + }; + + onTagChange2 = (val: string[]) => { + this.selectValue2.value = val; + }; + + onTagChange3 = (val: string[]) => { + this.selectValue3.value = val; + }; + + install() { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + {/* */} + + + {this.selectValue1.value.label} + + ) + } + panel={ +
      + {OPTIONS.map((item) => ( +
    • this.onOptionClick(item)}> + {item.label} +
    • + ))} +
    + } + onClear={this.onClear} + /> + +
    +
    + + {/* */} + ( + + {value} + + )} + panel={
    暂无示意数据
    } + onTagChange={this.onTagChange2} + /> + +
    +
    + + {/* */} + + value.map((item, index) => ( + onClose(index)} + > + + {value} + + + )) + } + panel={
    暂无示意数据
    } + onTagChange={this.onTagChange3} + /> +
    + ); + } +} diff --git a/src/select-input/_example/excess-tags-display-type.tsx b/src/select-input/_example/excess-tags-display-type.tsx new file mode 100644 index 0000000..b3eb547 --- /dev/null +++ b/src/select-input/_example/excess-tags-display-type.tsx @@ -0,0 +1,158 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-web-components/checkbox'; + +import { Component, signal } from 'omi'; +import { CheckboxGroupChangeContext, CheckboxOptionObj } from 'tdesign-web-components/checkbox'; +import { TagInputChangeContext, TagInputValue } from 'tdesign-web-components/tag-input'; + +const classStyles = ` + +`; + +const OPTIONS: CheckboxOptionObj[] = [ + // 全选 + { label: 'Check All', checkAll: true }, + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputExcessTagsDisplayType extends Component { + options = signal([...OPTIONS]); + + value = signal(OPTIONS.slice(1)); + + checkboxValue = (() => { + const arr = []; + const list = this.value.value; + // 此处不使用 forEach,减少函数迭代 + for (let i = 0, len = list.length; i < len; i++) { + list[i].value && arr.push(list[i].value); + } + return arr; + })(); + + // 直接 checkboxgroup 组件渲染输出下拉选项,自定义处理可以避免顺序和 tagChange 冲突 + onCheckedChange = (val: any, { current, type }: CheckboxGroupChangeContext) => { + // current 不存在,则表示操作全选 + if (!current) { + const newValue = type === 'check' ? this.options.value.slice(1) : []; + this.value.value = newValue; + return; + } + // 普通操作 + if (type === 'check') { + const option = this.options.value.find((t) => t.value === current); + this.value.value = this.value.value.concat(option); + } else { + const newValue = this.value.value.filter((v) => v.value !== current); + this.value.value = newValue; + } + }; + + // 可以根据触发来源,自由定制标签变化时的筛选器行为 + onTagChange = (currentTags: TagInputValue, context: TagInputChangeContext) => { + const { trigger, index, item } = context; + if (trigger === 'clear') { + this.value.value = []; + } + if (['tag-remove', 'backspace'].includes(trigger)) { + const newValue = [...this.value.value]; + newValue.splice(index, 1); + this.value.value = newValue; + } + // 如果允许创建新条目 + if (trigger === 'enter') { + const current = { label: item, value: item }; + const newValue = [...this.value.value]; + this.value.value = newValue.concat(current); + this.options.value = this.options.value.concat(current); + } + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + {/* */} +

    第一种呈现方式:超出时滚动显示

    +
    + + } + /> + +
    +
    +
    + + {/* */} +

    第二种呈现方式:超出时换行显示

    +
    + + } + /> +
    + ); + } +} diff --git a/src/select-input/_example/label-suffix.tsx b/src/select-input/_example/label-suffix.tsx new file mode 100644 index 0000000..d746a44 --- /dev/null +++ b/src/select-input/_example/label-suffix.tsx @@ -0,0 +1,126 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; +import { PopupVisibleChangeContext } from 'tdesign-web-components/popup'; + +const classStyles = ` + +`; + +const OPTIONS = [ + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputSingle extends Component { + selectValue = signal({ label: 'tdesign-vue', value: 1 }); + + popupVisible = signal(false); + + popupVisible2 = signal(false); + + onOptionClick = (item: { label: string; value: number }) => { + this.selectValue.value = item; + // 选中后立即关闭浮层 + this.popupVisible.value = false; + this.popupVisible2.value = false; + }; + + onClear = () => { + this.selectValue.value = undefined; + }; + + onPopupVisibleChange = (val: boolean, context: PopupVisibleChangeContext) => { + console.log(context); + this.popupVisible.value = val; + }; + + onPopupVisibleChange2 = (val: boolean) => { + this.popupVisible2.value = val; + }; + + installed(): void { + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + {/* */} + 前置内容:} + placeholder="Please Select" + clearable + popupProps={{ overlayInnerStyle: { padding: 6 } }} + onPopupVisibleChange={this.onPopupVisibleChange} + onClear={this.onClear} + panel={ +
      + {OPTIONS.map((item) => ( +
    • this.onOptionClick(item)}> + {item.label} +
    • + ))} +
    + } + suffixIcon={} + /> +

    + 单位:元} + placeholder="Please Select" + clearable + popupProps={{ overlayInnerStyle: { padding: 6 } }} + onPopupVisibleChange={this.onPopupVisibleChange2} + onClear={this.onClear} + panel={ +
      + {OPTIONS.map((item) => ( +
    • this.onOptionClick(item)}> + {item.label} +
    • + ))} +
    + } + suffixIcon={} + /> +
    + ); + } +} diff --git a/src/select-input/_example/multiple.tsx b/src/select-input/_example/multiple.tsx new file mode 100644 index 0000000..4e57a9c --- /dev/null +++ b/src/select-input/_example/multiple.tsx @@ -0,0 +1,198 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-web-components/checkbox'; +import 'tdesign-web-components/radio'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; +import type { CheckboxGroupProps, CheckboxOptionObj, SelectInputProps } from 'tdesign-web-components'; + +const classStyles = ` +.tdesign-demo__panel-options-multiple { + width: 100%; + padding: 0 !important; + display: flex !important; + flex-direction: column; + gap: 2px !important; +} +.tdesign-demo__panel-options-multiple .t-checkbox { + display: flex; + border-radius: 3px; + line-height: 22px; + cursor: pointer; + padding: 3px 8px; + color: var(--td-text-color-primary); + transition: background-color 0.2s linear; + white-space: nowrap; + word-wrap: normal; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; +} +.tdesign-demo__panel-options-multiple .t-checkbox:hover { + background-color: var(--td-bg-color-container-hover); +} +`; + +const OPTIONS: CheckboxOptionObj[] = [ + // 全选 + { label: 'Check All', checkAll: true, isLightDom: true }, + { label: 'tdesign-vue', value: 1, isLightDom: true }, + { label: 'tdesign-react', value: 2, isLightDom: true }, + { label: 'tdesign-miniprogram', value: 3, isLightDom: true }, + { label: 'tdesign-angular', value: 4, isLightDom: true }, + { label: 'tdesign-mobile-vue', value: 5, isLightDom: true }, + { label: 'tdesign-mobile-react', value: 6, isLightDom: true }, +]; + +type ExcessTagsDisplayType = SelectInputProps['tagInputProps']['excessTagsDisplayType']; + +export default class SelectInputMultiple extends Component { + excessTagsDisplayType = signal('break-line'); + + allowInput = signal(true); + + creatable = signal(true); + + inputValue = signal(''); + + // 全量数据 + options = signal([...OPTIONS]); + + // 仅用作展示的数据(过滤功能需要使用) + displayOptions = signal([...OPTIONS]); + + value = signal>([ + { label: 'Vue', value: 1 }, + { label: 'React', value: 2 }, + { label: 'Miniprogram', value: 3 }, + ]); + + getCheckboxValue = () => { + const arr = []; + const list = this.value.value; + // 此处不使用 forEach,减少函数迭代 + for (let i = 0, len = list.length; i < len; i++) { + list[i].value && arr.push(list[i].value); + } + return arr; + }; + + // 直接 checkboxgroup 组件渲染输出下拉选项,自定义处理可以避免顺序和 tagChange 冲突 + onCheckedChange: CheckboxGroupProps['onChange'] = (val, { current, type }) => { + // current 不存在,则表示操作全选 + if (!current) { + const newValue = type === 'check' ? this.options.value.slice(1) : []; + this.value.value = newValue; + return; + } + // 普通操作 + if (type === 'check') { + const option = this.options.value.find((t) => t.value === current); + this.value.value = this.value.value.concat(option); + } else { + const newValue = this.value.value.filter((v) => v.value !== current); + this.value.value = newValue; + } + }; + + // 可以根据触发来源,自由定制标签变化时的筛选器行为 + onTagChange: SelectInputProps['onTagChange'] = (currentTags, context) => { + const { trigger, index } = context; + if (trigger === 'clear') { + this.value.value = []; + } + if (['tag-remove', 'backspace'].includes(trigger)) { + const newValue = [...this.value.value]; + newValue.splice(index, 1); + this.value.value = newValue; + } + }; + + onInputChange: SelectInputProps['onInputChange'] = (val, context) => { + this.inputValue.value = val; + // 过滤功能 + console.log(val, context); + }; + + onInputEnter: SelectInputProps['onEnter'] = (_, { inputValue }) => { + // 如果允许创建新条目 + if (this.creatable.value) { + const current = { label: inputValue, value: inputValue }; + const newValue = [...this.value.value]; + this.value.value = newValue.concat(current); + const newOptions = this.options.value.concat(current); + this.options.value = newOptions; + this.displayOptions.value = newOptions; + this.inputValue.value = ''; + } + }; + + render() { + const checkboxValue = this.getCheckboxValue(); + return ( +
    +
    + { + this.allowInput.value = v; + }} + > + 是否允许输入 + + { + this.creatable.value = v; + }} + > + 允许创建新选项(Enter 创建) + +
    +
    +
    + (this.excessTagsDisplayType.value = val)} + options={[ + { label: '选中项过多横向滚动', value: 'scroll' }, + { label: '选中项过多换行显示', value: 'break-line' }, + ]} + /> +
    +
    +
    + + {/* */} + 多选:} + panel={ + this.displayOptions.value.length ? ( + + ) : ( +
    暂无数据
    + ) + } + suffixIcon={} + clearable + multiple + onTagChange={this.onTagChange} + onInputChange={this.onInputChange} + onEnter={this.onInputEnter} + /> +
    + ); + } +} diff --git a/src/select-input/_example/single.tsx b/src/select-input/_example/single.tsx new file mode 100644 index 0000000..9c4ff42 --- /dev/null +++ b/src/select-input/_example/single.tsx @@ -0,0 +1,94 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-icons-web-components/esm/components/chevron-down'; + +import { Component, signal } from 'omi'; + +const classStyles = ` + +`; + +const OPTIONS = [ + { label: 'tdesign-vue', value: 1 }, + { label: 'tdesign-react', value: 2 }, + { label: 'tdesign-miniprogram', value: 3 }, + { label: 'tdesign-angular', value: 4 }, + { label: 'tdesign-mobile-vue', value: 5 }, + { label: 'tdesign-mobile-react', value: 6 }, +]; + +export default class SelectInputSingle extends Component { + selectValue = signal({ label: 'tdesign-vue', value: 1 }); + + popupVisible = signal(false); + + onOptionClick = (e: Event, item: { label: string; value: number }) => { + // e.stopPropagation(); + this.selectValue.value = item; + // 选中后立即关闭浮层 + this.popupVisible.value = false; + }; + + onClear = () => { + this.selectValue.value = undefined; + }; + + onPopupVisibleChange = (val) => { + this.popupVisible.value = val; + }; + + installed(): void { + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( +
    + + {OPTIONS.map((item) => ( +
  • this.onOptionClick(e, item)}> + {item.label} +
  • + ))} + + } + suffixIcon={} + /> +
    + ); + } +} diff --git a/src/select-input/_example/status.tsx b/src/select-input/_example/status.tsx new file mode 100644 index 0000000..c64a16f --- /dev/null +++ b/src/select-input/_example/status.tsx @@ -0,0 +1,103 @@ +import 'tdesign-web-components/select-input'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +const classStyles = ` + +`; + +export default class SelectInputStatus extends Component { + selectValue = 'TDesign'; + + installed(): void { + document.head.insertAdjacentHTML('beforeend', classStyles); + } + + render() { + return ( + + + 禁用状态: + 暂无数据
    } + /> + + + + 只读状态: + 暂无数据
    } + /> + + + + 成功状态: + 暂无数据
    } + /> +
    + + + 警告状态: + 暂无数据
    } + /> + + + + 错误状态: + 暂无数据} + /> + + + + 加载状态: + 加载中...} + /> + + + ); + } +} diff --git a/src/select-input/_example/width.tsx b/src/select-input/_example/width.tsx new file mode 100644 index 0000000..e41930f --- /dev/null +++ b/src/select-input/_example/width.tsx @@ -0,0 +1,88 @@ +import 'tdesign-web-components/select-input'; + +const classStyles = ` + +`; + +export default function SelectInputWidth() { + const selectValue = 'TDesign'; + + // 添加示例代码所需样式 + document.head.insertAdjacentHTML('beforeend', classStyles); + + return ( +
    +
    + 下拉框默认宽度: + 下拉框宽度和触发元素宽度保持一致(默认)
    } + > +
    +
    + +
    + 下拉框最大宽度: + + 下拉框宽度和触发元素宽度保持一致,但是当下拉框内容宽度超出时,自动撑开下拉框宽度,最大不超过 + 1000px(默认) +
    + } + > + +
    + +
    + 与内容宽度一致: + 宽度随内容宽度自适应
    } + > + +
    + +
    + 下拉框固定宽度: + 固定宽度 360px
    } + > + +
    + + ); +} diff --git a/src/select-input/defaultProps.ts b/src/select-input/defaultProps.ts new file mode 100644 index 0000000..c735a60 --- /dev/null +++ b/src/select-input/defaultProps.ts @@ -0,0 +1,19 @@ +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdSelectInputProps } from './type'; + +export const selectInputDefaultProps: TdSelectInputProps = { + allowInput: false, + autoWidth: false, + autofocus: false, + borderless: false, + clearable: false, + loading: false, + minCollapsedNum: 0, + multiple: false, + readonly: false, + reserveKeyword: false, + status: 'default', +}; diff --git a/src/select-input/index.tsx b/src/select-input/index.tsx new file mode 100644 index 0000000..028b9f6 --- /dev/null +++ b/src/select-input/index.tsx @@ -0,0 +1,9 @@ +import './style/index.js'; + +import _SelectInput from './SelectInput'; + +export type { SelectInputProps } from './SelectInput'; +export * from './type'; + +export const SelectInput = _SelectInput; +export default SelectInput; diff --git a/src/select-input/interface.ts b/src/select-input/interface.ts new file mode 100644 index 0000000..48b68b0 --- /dev/null +++ b/src/select-input/interface.ts @@ -0,0 +1,18 @@ +import { TdSelectInputProps } from './type'; + +export interface SelectInputCommonProperties { + status?: TdSelectInputProps['status']; + tips?: TdSelectInputProps['tips']; + clearable?: TdSelectInputProps['clearable']; + disabled?: TdSelectInputProps['disabled']; + label?: TdSelectInputProps['label']; + placeholder?: TdSelectInputProps['placeholder']; + readonly?: TdSelectInputProps['readonly']; + suffix?: TdSelectInputProps['suffix']; + suffixIcon?: TdSelectInputProps['suffixIcon']; + size?: TdSelectInputProps['size']; + onPaste?: TdSelectInputProps['onPaste']; + onEnter?: TdSelectInputProps['onEnter']; + onMouseenter?: TdSelectInputProps['onMouseenter']; + onMouseleave?: TdSelectInputProps['onMouseleave']; +} diff --git a/src/select-input/style/index.js b/src/select-input/style/index.js new file mode 100644 index 0000000..e927dae --- /dev/null +++ b/src/select-input/style/index.js @@ -0,0 +1,9 @@ +import { css, globalCSS } from 'omi'; + +import styles from '../../_common/style/web/components/select-input/_index.less'; + +export const styleSheet = css` + ${styles} +`; + +globalCSS(styleSheet); diff --git a/src/select-input/type.ts b/src/select-input/type.ts new file mode 100644 index 0000000..b2966f9 --- /dev/null +++ b/src/select-input/type.ts @@ -0,0 +1,231 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { InputProps } from '../input'; +import { PopupProps } from '../popup'; +import { TagInputProps, TagInputValue, TagInputChangeContext } from '../tag-input'; +import { TagProps } from '../tag'; +import { PopupVisibleChangeContext } from '../popup'; +import { TNode, TElement, SizeEnum } from '../common'; + +export interface TdSelectInputProps { + /** + * 是否允许输入 + * @default false + */ + allowInput?: boolean; + /** + * 宽度随内容自适应 + * @default false + */ + autoWidth?: boolean; + /** + * 自动聚焦 + * @default false + */ + autofocus?: boolean; + /** + * 无边框模式 + * @default false + */ + borderless?: boolean; + /** + * 是否可清空 + * @default false + */ + clearable?: boolean; + /** + * 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 `collapsedItems` 自定义。`value` 表示所有标签值,`collapsedSelectedItems` 表示折叠标签值,`count` 表示折叠的数量,`onClose` 表示移除标签的事件回调 + */ + collapsedItems?: TNode<{ + value: SelectInputValue; + collapsedSelectedItems: SelectInputValue; + count: number; + onClose: (context: { index: number; e?: MouseEvent }) => void; + }>; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 透传 Input 输入框组件全部属性 + */ + inputProps?: InputProps; + /** + * 输入框的值 + */ + inputValue?: string; + /** + * 输入框的值,非受控属性 + */ + defaultInputValue?: string; + /** + * 定义字段别名,示例:`{ label: 'text', value: 'id', children: 'list' }` + */ + keys?: SelectInputKeys; + /** + * 左侧文本 + */ + label?: TNode; + /** + * 是否处于加载状态 + * @default false + */ + loading?: boolean; + /** + * 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 + * @default 0 + */ + minCollapsedNum?: number; + /** + * 是否为多选模式,默认为单选 + * @default false + */ + multiple?: boolean; + /** + * 下拉框内容,可完全自定义 + */ + panel?: TNode; + /** + * 占位符 + * @default '' + */ + placeholder?: string; + /** + * 透传 Popup 浮层组件全部属性 + */ + popupProps?: PopupProps; + /** + * 是否显示下拉框 + */ + popupVisible?: boolean; + /** + * 是否显示下拉框,非受控属性 + */ + defaultPopupVisible?: boolean; + /** + * 组件前置图标 + */ + prefixIcon?: TElement; + /** + * 只读状态,值为真会隐藏输入框,且无法打开下拉框 + * @default false + */ + readonly?: boolean; + /** + * 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 + * @default false + */ + reserveKeyword?: boolean; + /** + * 组件尺寸 + * @default medium + */ + size?: SizeEnum; + /** + * 输入框状态 + * @default default + */ + status?: 'default' | 'success' | 'warning' | 'error'; + /** + * 后置图标前的后置内容 + */ + suffix?: TNode; + /** + * 组件后置图标 + */ + suffixIcon?: TNode; + /** + * 多选场景下,自定义选中标签的内部内容。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签 + */ + tag?: string | TNode<{ value: string | number }>; + /** + * 透传 TagInput 组件全部属性 + */ + tagInputProps?: TagInputProps; + /** + * 透传 Tag 标签组件全部属性 + */ + tagProps?: TagProps; + /** + * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 + */ + tips?: TNode; + /** + * 全部标签值。值为数组表示多个标签,值为非数组表示单个数值 + */ + value?: SelectInputValue; + /** + * 自定义值呈现的全部内容,参数为所有标签的值 + */ + valueDisplay?: string | TNode<{ value: TagInputValue; onClose: (index: number, item?: any) => void }>; + /** + * 失去焦点时触发,`context.inputValue` 表示输入框的值;`context.tagInputValue` 表示标签输入框的值 + */ + onBlur?: (value: SelectInputValue, context: SelectInputBlurContext) => void; + /** + * 清空按钮点击时触发 + */ + onClear?: (context: { e: MouseEvent }) => void; + /** + * 按键按下 Enter 时触发 + */ + onEnter?: ( + value: SelectInputValue, + context: { e: KeyboardEvent; inputValue: string; tagInputValue?: TagInputValue }, + ) => void; + /** + * 聚焦时触发 + */ + onFocus?: (value: SelectInputValue, context: SelectInputFocusContext) => void; + /** + * 输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发等 + */ + onInputChange?: (value: string, context?: SelectInputValueChangeContext) => void; + /** + * 进入输入框时触发 + */ + onMouseenter?: (context: { e: MouseEvent }) => void; + /** + * 离开输入框时触发 + */ + onMouseleave?: (context: { e: MouseEvent }) => void; + /** + * 粘贴事件,`pasteValue` 表示粘贴板的内容 + */ + onPaste?: (context: { e: ClipboardEvent; pasteValue: string }) => void; + /** + * 下拉框显示或隐藏时触发 + */ + onPopupVisibleChange?: (visible: boolean, context: PopupVisibleChangeContext) => void; + /** + * 值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数 + */ + onTagChange?: (value: TagInputValue, context: SelectInputChangeContext) => void; +} + +export interface SelectInputKeys { + label?: string; + value?: string; + children?: string; +} + +export type SelectInputValue = string | number | boolean | Date | Object | Array | Array; + +export type SelectInputBlurContext = PopupVisibleChangeContext & { inputValue: string; tagInputValue?: TagInputValue }; + +export interface SelectInputFocusContext { + inputValue: string; + tagInputValue?: TagInputValue; + e: FocusEvent; +} + +export interface SelectInputValueChangeContext { + e?: Event | MouseEvent | FocusEvent | KeyboardEvent | CompositionEvent; + trigger: 'input' | 'clear' | 'blur' | 'focus' | 'initial' | 'change'; +} + +export type SelectInputChangeContext = TagInputChangeContext; diff --git a/src/select-input/useOverlayInnerStyle.ts b/src/select-input/useOverlayInnerStyle.ts new file mode 100644 index 0000000..37f486e --- /dev/null +++ b/src/select-input/useOverlayInnerStyle.ts @@ -0,0 +1,99 @@ +import isFunction from 'lodash/isFunction'; +import isObject from 'lodash/isObject'; +import { Component } from 'omi'; + +import useControlled from '../_util/useControlled'; +import { PopupVisibleChangeContext, TdPopupProps } from '../popup'; +import { TdSelectInputProps } from './type'; + +export type overlayStyleProps = Pick< + TdSelectInputProps, + | 'popupProps' + | 'autoWidth' + | 'readonly' + | 'onPopupVisibleChange' + | 'disabled' + | 'allowInput' + | 'popupVisible' + | 'defaultPopupVisible' +>; + +// 单位:px +const MAX_POPUP_WIDTH = 1000; + +export default function useOverlayInnerStyle( + props: overlayStyleProps, + extra?: { + afterHidePopup?: (ctx: PopupVisibleChangeContext) => void; + }, + activeComponent?: Component, +) { + const { popupProps, autoWidth, readonly, disabled, onPopupVisibleChange, allowInput } = props; + const [innerPopupVisible, setInnerPopupVisible] = useControlled(props, 'popupVisible', onPopupVisibleChange, { + activeComponent, + }); + + const matchWidthFunc = (triggerElement: HTMLElement, popupElement: HTMLElement) => { + if (!triggerElement || !popupElement) return; + + // 设置display来可以获取popupElement的宽度 + // eslint-disable-next-line no-param-reassign + popupElement.style.display = ''; + // popupElement的scrollBar宽度 + const overlayScrollWidth = popupElement.offsetWidth - popupElement.scrollWidth; + + /** + * issue:https://github.com/Tencent/tdesign-react/issues/2642 + * + * popupElement的内容宽度不超过triggerElement的宽度,就使用triggerElement的宽度减去popup的滚动条宽度, + * 让popupElement的宽度加上scrollBar的宽度等于triggerElement的宽度; + * + * popupElement的内容宽度超过triggerElement的宽度,就使用popupElement的scrollWidth, + * 不用offsetWidth是会包含scrollBar的宽度 + */ + const width = + popupElement.offsetWidth - overlayScrollWidth > triggerElement.offsetWidth + ? popupElement.scrollWidth + : triggerElement.offsetWidth - overlayScrollWidth; + + let otherOverlayInnerStyle = {}; + if (popupProps && typeof popupProps.overlayInnerStyle === 'object' && !popupProps.overlayInnerStyle.width) { + otherOverlayInnerStyle = popupProps.overlayInnerStyle; + } + return { + width: `${Math.min(width, MAX_POPUP_WIDTH)}px`, + ...otherOverlayInnerStyle, + }; + }; + + const onInnerPopupVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => { + if (disabled || readonly) { + return; + } + // 如果点击触发元素(输入框)且为可输入状态,则继续显示下拉框 + const newVisible = context.trigger === 'trigger-element-click' && allowInput ? true : visible; + if (props.popupVisible !== newVisible) { + setInnerPopupVisible(newVisible, context); + if (!newVisible) { + extra?.afterHidePopup?.(context); + } + } + }; + + const tOverlayInnerStyle = () => { + let result: TdPopupProps['overlayInnerStyle'] = {}; + const overlayInnerStyle = popupProps?.overlayInnerStyle || {}; + if (isFunction(overlayInnerStyle) || (isObject(overlayInnerStyle) && overlayInnerStyle.width)) { + result = overlayInnerStyle; + } else if (!autoWidth) { + result = matchWidthFunc; + } + return result; + }; + + return { + tOverlayInnerStyle, + innerPopupVisible, + onInnerPopupVisibleChange, + }; +} diff --git a/src/tag-input/style/index.js b/src/tag-input/style/index.js new file mode 100644 index 0000000..9af4143 --- /dev/null +++ b/src/tag-input/style/index.js @@ -0,0 +1,9 @@ +import { css, globalCSS } from 'omi'; + +import styles from '../../_common/style/web/components/tag-input/_index.less'; + +export const styleSheet = css` + ${styles} +`; + +globalCSS(styleSheet); diff --git a/src/tag-input/style/index.ts b/src/tag-input/style/index.ts deleted file mode 100644 index 0ba4125..0000000 --- a/src/tag-input/style/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { css, globalCSS } from 'omi'; - -import inputStyles from '../../_common/style/web/components/input/_index.less'; -import tagStyles from '../../_common/style/web/components/tag/_index.less'; -import styles from '../../_common/style/web/components/tag-input/_index.less'; -import theme from '../../_common/style/web/theme/_index.less'; - -export const styleSheet = css` - ${styles} - ${inputStyles} - ${tagStyles} - ${theme} -`; - -globalCSS(styleSheet); diff --git a/src/tag-input/tag-input.tsx b/src/tag-input/tag-input.tsx index c8818dc..8bf324a 100644 --- a/src/tag-input/tag-input.tsx +++ b/src/tag-input/tag-input.tsx @@ -344,7 +344,6 @@ export default class TagInput extends Component { }; private onInnerClick = (context: { e: MouseEvent }) => { - console.log('innerClick'); const { props, tagInputRef } = this; if (!props.disabled && !props.readonly) { (tagInputRef.current as any).inputElement?.focus?.(); @@ -422,7 +421,8 @@ export default class TagInput extends Component { status, suffixIcon, suffix, - style, + innerStyle, + borderless, onPaste, onFocus, onBlur, @@ -441,7 +441,7 @@ export default class TagInput extends Component { // 自定义 Tag 节点 const displayNode = isFunction(valueDisplay) - ? valueDisplay({ + ? (valueDisplay as any)({ value: tagValue, onClose: (index, item) => this.onClose({ index, item }), }) @@ -457,7 +457,7 @@ export default class TagInput extends Component { const list = displayNode ? displayNode : newList?.map((item, index) => { - const tagContent = isFunction(props.tag) ? props.tag({ value: item }) : props.tag; + const tagContent = isFunction(props.tag) ? (props.tag as any)({ value: item }) : props.tag; return ( { collapsedSelectedItems: tagValue.slice(props.minCollapsedNum, tagValue.length), onClose: this.onClose, }; - const more = isFunction(props.collapsedItems) ? props.collapsedItems(params) : props.collapsedItems; + const more = isFunction(props.collapsedItems) ? (props.collapsedItems as any)(params) : props.collapsedItems; if (more) { list.push(more); } else { - list.push(+{len}); + list.push( + + +{len} + , + ); } } return list; @@ -508,7 +515,7 @@ export default class TagInput extends Component { const suffixIconNode = showClearIcon ? ( { [`${classPrefix}-input--auto-width`]: !!autoWidth, [`${classPrefix}-input__warp`]: !autoWidth, }, - props.className, + props.innerClass, ]; return ( @@ -549,8 +556,8 @@ export default class TagInput extends Component { readonly={readonly} disabled={disabled} label={renderLabel({ displayNode, label })} - class={classNames(classes)} - style={style} + innerClass={classNames(classes)} + style={innerStyle} tips={tips} status={status} placeholder={tagInputPlaceholder} @@ -558,6 +565,7 @@ export default class TagInput extends Component { suffixIcon={suffixIconNode} showInput={!inputProps?.readonly || !tagValue || !tagValue?.length} keepWrapperWidth={!autoWidth} + borderless={borderless} onPaste={onPaste} onEnter={this.onInputEnter} onMyKeydown={this.onInputBackspaceKeyDown} From b20304932aa9121f617daa0f33f90d90b1c0e70e Mon Sep 17 00:00:00 2001 From: ming680 <827668037@qq.com> Date: Wed, 25 Sep 2024 15:10:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(menu):=20fix=20=E6=94=B6=E8=B5=B7?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/menu/MenuItem.tsx | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/menu/MenuItem.tsx b/src/menu/MenuItem.tsx index e6780e9..421b766 100644 --- a/src/menu/MenuItem.tsx +++ b/src/menu/MenuItem.tsx @@ -32,14 +32,6 @@ export default class MenuItem extends Component { position: absolute; inset: 0; } - - .${getClassPrefix()}-menu__item .${getClassPrefix()}-menu__item-tooltip-inner { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - } `; static propTypes = { @@ -108,14 +100,15 @@ export default class MenuItem extends Component { ); - if (this.injection.collapsed.value && !this.props.disabled) { - return ( - -
    {content}
    -
    - ); - } - - return
    {content}
    ; + return ( + <> +
    {content}
    + {this.injection.collapsed.value && !this.props.disabled ? ( + +
    + + ) : null} + + ); } }