}`;
+
+const useControlled: (
+ props: R,
+ valueKey: K,
+ onChange: ChangeHandler,
+ defaultOptions?:
+ | {
+ [key in Defaultoptions>]: R[K];
+ }
+ | object,
+) => [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)}`];
+
+ // 无论是否受控,都要维护一个内部变量,默认值由 defaultValue 控制
+ const internalValue = signal(defaultValue);
+ // 受控模式
+ console.log('===controlled', controlled, valueKey);
+ if (controlled) return [value, onChange || (() => {})];
+
+ // 非受控模式
+ return [
+ internalValue.value,
+ (newValue, ...args) => {
+ console.log('===newValue', newValue);
+ internalValue.value = newValue;
+ onChange?.(newValue, ...args);
+ },
+ ];
+};
+
+export default useControlled;
diff --git a/src/index.ts b/src/index.ts
index c951a7e..89ee5e8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,6 +24,7 @@ export * from './popconfirm';
export * from './popup';
export * from './radio';
export * from './range-input';
+export * from './select-input';
export * from './slider';
export * from './space';
export * from './swiper';
diff --git a/src/input/_example/clearable.tsx b/src/input/_example/clearable.tsx
new file mode 100644
index 0000000..edc1392
--- /dev/null
+++ b/src/input/_example/clearable.tsx
@@ -0,0 +1,27 @@
+import 'tdesign-web-components/input';
+
+import { Component } from 'omi';
+
+export default class InputExample extends Component {
+ value = 'TDesign';
+
+ onChange = (v) => {
+ this.value = v;
+ };
+
+ render() {
+ return (
+ {
+ this.onChange(value);
+ }}
+ onClear={() => {
+ console.log('onClear');
+ }}
+ />
+ );
+ }
+}
diff --git a/src/input/input.tsx b/src/input/input.tsx
index 516de72..f85a290 100644
--- a/src/input/input.tsx
+++ b/src/input/input.tsx
@@ -138,13 +138,14 @@ export default class Input extends Component {
};
private handleFocus = (e: FocusEvent) => {
+ console.log('===e', e.composedPath());
e.stopImmediatePropagation();
const { readonly, onFocus } = this.props;
if (readonly) return;
const { currentTarget }: { currentTarget: any } = e;
onFocus?.(currentTarget.value, { e });
this.isFocused = true;
- this.update();
+ // (this as any).queuedUpdate();
};
private handleBlur = (e: FocusEvent) => {
@@ -172,6 +173,7 @@ export default class Input extends Component {
};
private handleClear = (e: MouseEvent) => {
+ console.log('---clear');
const { onChange, onClear } = this.props;
this.composingValue = '';
this.value = '';
@@ -309,7 +311,7 @@ export default class Input extends Component {
onValidate,
});
- const isShowClearIcon = ((clearable && this.value && !disabled) || showClearIconOnEmpty) && this.isHover;
+ const isShowClearIcon = (clearable && this.value && !disabled) || showClearIconOnEmpty;
const prefixIconContent = renderIcon('t', 'prefix', parseTNode(prefixIcon));
let suffixIconNew = suffixIcon;
diff --git a/src/popconfirm/popcontent.tsx b/src/popconfirm/popcontent.tsx
index 714969c..d31f0d3 100644
--- a/src/popconfirm/popcontent.tsx
+++ b/src/popconfirm/popcontent.tsx
@@ -1,4 +1,4 @@
-import 'tdesign-icons-web-components';
+import 'tdesign-icons-web-components/esm/components/info-circle-filled';
import isString from 'lodash/isString';
import { cloneElement, Component, OmiProps, tag, VNode } from 'omi';
@@ -47,7 +47,7 @@ export default class Popconfirm extends Component;
+ const defaultIcon = ;
switch (this.props.theme) {
case 'warning': // 黄色
color = '#FFAA00';
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..a1b4aae
--- /dev/null
+++ b/src/select-input/SelectInput.tsx
@@ -0,0 +1,149 @@
+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 { 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',
+];
+
+@tag('t-select-input')
+class SelectInput extends Component {
+ static defaultProps = selectInputDefaultProps;
+
+ selectInputRef = createRef();
+
+ selectInputWrapRef = createRef();
+
+ classPrefix = getClassPrefix();
+
+ commonInputProps: SelectInputCommonProperties;
+
+ tOverlayInnerStyle;
+
+ innerPopupVisible;
+
+ onInnerPopupVisibleChange;
+
+ install(): void {
+ const { loading, suffixIcon } = this.props;
+ this.commonInputProps = {
+ ...pick(this.props, COMMON_PROPERTIES),
+ suffixIcon: loading ? : suffixIcon,
+ };
+
+ const { innerPopupVisible, tOverlayInnerStyle, onInnerPopupVisibleChange } = useOverlayInnerStyle(this.props, {
+ // afterHidePopup: this.onInnerBlur,
+ });
+ this.tOverlayInnerStyle = tOverlayInnerStyle;
+ this.innerPopupVisible = innerPopupVisible;
+ this.onInnerPopupVisibleChange = onInnerPopupVisibleChange;
+ }
+
+ // onInnerBlur(ctx: PopupVisibleChangeContext) {
+ // const inputValue = this.props.multiple ? multipleInputValue : 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.className,
+ `${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 = (
+ console.log('333', e)}>
+
+ {multiple ? (
+
+ ) : (
+
+ )}
+
+
+ );
+
+ if (!props.tips) {
+ return mainContent;
+ }
+
+ return (
+ console.log('222', e)}
+ >
+ {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..afd243f
--- /dev/null
+++ b/src/select-input/SelectInputMultiple.tsx
@@ -0,0 +1,100 @@
+import '../tag-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 { 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',
+};
+
+@tag('t-select-input-multiple')
+export default class SelectInputMultiple extends Component {
+ classPrefix = getClassPrefix();
+
+ tagInputRef = createRef();
+
+ render(props) {
+ const { value, popupVisible, commonInputProps, allowInput } = props;
+ const [tInputValue, setTInputValue] = useControlled(props, 'inputValue', props.onInputChange);
+ 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 onTagInputChange = (val: TagInputValue, context: SelectInputChangeContext) => {
+ // 避免触发浮层的显示或隐藏
+ if (context.trigger === 'tag-remove') {
+ context.e?.stopPropagation();
+ }
+ props.onTagChange?.(val, context);
+ };
+
+ const onInnerClear = (context: { e: MouseEvent }) => {
+ console.log('====cle');
+ context?.e?.stopPropagation();
+ props.onClear?.(context);
+ setTInputValue('', { trigger: 'clear' });
+ };
+
+ return (
+ {
+ // 筛选器统一特性:筛选器按下回车时不清空输入框
+ if (context?.trigger === 'enter' || context?.trigger === 'blur') return;
+ setTInputValue(val, { trigger: context.trigger, e: context.e });
+ }}
+ tagProps={props.tagProps}
+ onClear={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}
+ {...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,
+ }),
+ }}
+ />
+ );
+ }
+}
diff --git a/src/select-input/SelectInputSingle.tsx b/src/select-input/SelectInputSingle.tsx
new file mode 100644
index 0000000..fbcd706
--- /dev/null
+++ b/src/select-input/SelectInputSingle.tsx
@@ -0,0 +1,101 @@
+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 {
+ classPrefix = getClassPrefix();
+
+ inputRef = createRef();
+
+ inputValue;
+
+ setInputValue;
+
+ install(): void {
+ const [inputValue, setInputValue] = useControlled(this.props, 'inputValue', this.props.onInputChange);
+ this.inputValue = inputValue;
+ this.setInputValue = setInputValue;
+ }
+
+ render(props) {
+ const { value, keys, commonInputProps, popupVisible } = props;
+
+ const onInnerClear = (context: { e: MouseEvent }) => {
+ console.log('---fff');
+ context?.e?.stopPropagation();
+ props.onClear?.(context);
+ this.setInputValue('', { trigger: 'clear' });
+ };
+
+ const onInnerInputChange: TdInputProps['onChange'] = (value, context) => {
+ if (props.allowInput) {
+ this.setInputValue(value, { ...context, trigger: 'input' });
+ }
+ };
+
+ const handleEmptyPanelBlur = (value: string, { e }: { e: FocusEvent }) => {
+ props.onBlur?.(value, { e, inputValue: value });
+ };
+
+ // 单选,值的呈现方式
+ const singleValueDisplay = !props.multiple ? props.valueDisplay : null;
+ const displayedValue = popupVisible && props.allowInput ? this.inputValue : getInputValue(value, keys);
+ return (
+
+ {props.label}
+ {singleValueDisplay}
+ >
+ }
+ onChange={onInnerInputChange}
+ readonly={!props.allowInput}
+ onClear={onInnerClear}
+ // [Important Info]: SelectInput.blur is not equal to Input, example: click popup panel
+ onFocus={(val, context) => {
+ console.log('focus');
+ 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 ? handleEmptyPanelBlur : null}
+ {...props.inputProps}
+ inputClass={classNames(props.inputProps?.className, {
+ [`${this.classPrefix}-input--focused`]: popupVisible,
+ [`${this.classPrefix}-is-focused`]: popupVisible,
+ })}
+ />
+ );
+ }
+}
diff --git a/src/select-input/_example/single.tsx b/src/select-input/_example/single.tsx
new file mode 100644
index 0000000..9cdd9cb
--- /dev/null
+++ b/src/select-input/_example/single.tsx
@@ -0,0 +1,91 @@
+import 'tdesign-web-components/select-input';
+import 'tdesign-icons-web-components/esm/components/chevron-down';
+
+import { Component, signal } from 'omi';
+
+const classStyles = `
+.tdesign-demo__select-input-ul-single {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ gap: 2px;
+}
+.tdesign-demo__select-input-ul-single > li {
+ display: block;
+ 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;
+}
+
+.tdesign-demo__select-input-ul-single > li:hover {
+ background-color: var(--td-bg-color-container-hover);
+}
+`;
+
+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 {
+ static css = classStyles;
+
+ selectValue = signal({ label: 'tdesign-vue', value: 1 });
+
+ popupVisible = signal(false);
+
+ onOptionClick = (e: Event, item: { label: string; value: number }) => {
+ e.stopPropagation();
+ console.log('===item', item);
+ this.selectValue.value = item;
+ // 选中后立即关闭浮层
+ this.popupVisible.value = false;
+ };
+
+ onClear = () => {
+ this.selectValue.value = undefined;
+ };
+
+ onPopupVisibleChange = (val) => {
+ this.popupVisible.value = val;
+ };
+
+ render() {
+ return (
+
+
+ {OPTIONS.map((item) => (
+ this.onOptionClick(e, item)}>
+ {item.label}
+
+ ))}
+
+ }
+ suffixIcon={}
+ />
+
+ );
+ }
+}
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..72b5677
--- /dev/null
+++ b/src/select-input/style/index.js
@@ -0,0 +1 @@
+import '../../_common/style/web/components/select-input/_index.less';
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..40c0012
--- /dev/null
+++ b/src/select-input/useOverlayInnerStyle.ts
@@ -0,0 +1,96 @@
+import isFunction from 'lodash/isFunction';
+import isObject from 'lodash/isObject';
+
+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;
+ },
+) {
+ const { popupProps, autoWidth, readonly, disabled, onPopupVisibleChange, allowInput } = props;
+ const [innerPopupVisible, setInnerPopupVisible] = useControlled(props, 'popupVisible', onPopupVisibleChange);
+
+ 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,
+ };
+}
From 8551d7ac02bcd50e0e436c6c67d5eeed4285777e Mon Sep 17 00:00:00 2001
From: duenyang <377153400@qq.com>
Date: Fri, 13 Sep 2024 16:48:03 +0800
Subject: [PATCH 07/15] refactor(input): restore input
---
src/input/input.tsx | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/input/input.tsx b/src/input/input.tsx
index a6cc29c..a44e828 100644
--- a/src/input/input.tsx
+++ b/src/input/input.tsx
@@ -149,14 +149,13 @@ export default class Input extends Component {
};
private handleFocus = (e: FocusEvent) => {
- console.log('===e', e.composedPath());
e.stopImmediatePropagation();
const { readonly, onFocus } = this.props;
if (readonly) return;
const { currentTarget }: { currentTarget: any } = e;
onFocus?.(currentTarget.value, { e });
this.isFocused = true;
- // (this as any).queuedUpdate();
+ this.update();
};
private handleBlur = (e: FocusEvent) => {
@@ -186,7 +185,6 @@ export default class Input extends Component {
};
private handleClear = (e: MouseEvent) => {
- console.log('---clear');
const { onChange, onClear } = this.props;
this.composingValue = '';
this.value = '';
@@ -331,7 +329,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' },
}),
)
@@ -342,7 +340,7 @@ export default class Input extends Component {
suffixIconNew = (
{
suffixIconNew = (
{
suffixIconNew = (
Date: Sat, 14 Sep 2024 18:45:13 +0800
Subject: [PATCH 08/15] =?UTF-8?q?perf(popup):=20=E4=BC=98=E5=8C=96popup=20?=
=?UTF-8?q?dom=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/checkbox/checkbox-group.tsx | 34 +++--
src/checkbox/index.ts | 2 +
src/checkbox/type.ts | 4 +
src/popup/popup.tsx | 107 ++++++++++----
src/select-input/_example/multiple.tsx | 197 +++++++++++++++++++++++++
src/select-input/style/index.js | 10 +-
src/tag-input/style/index.js | 9 ++
src/tag-input/style/index.ts | 15 --
src/tag-input/tag-input.tsx | 2 +-
9 files changed, 321 insertions(+), 59 deletions(-)
create mode 100644 src/select-input/_example/multiple.tsx
create mode 100644 src/tag-input/style/index.js
delete mode 100644 src/tag-input/style/index.ts
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/popup/popup.tsx b/src/popup/popup.tsx
index a8b84a1..ffc3719 100644
--- a/src/popup/popup.tsx
+++ b/src/popup/popup.tsx
@@ -3,12 +3,13 @@ import './popupTrigger';
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 +41,6 @@ export const PopupTypes = {
onScroll: Function,
onScrollToBottom: Function,
onVisibleChange: Function,
- strategy: String,
expandAnimation: Boolean,
updateScrollTop: Function,
};
@@ -65,7 +65,6 @@ export default class Popup extends Component
{
placement: 'top',
showArrow: true,
trigger: 'hover',
- strategy: 'fixed',
};
triggerRef = createRef();
@@ -81,9 +80,11 @@ export default class Popup extends Component {
hasDocumentEvent = false;
visible = false;
- // watch visible TODO:
- hasTrigger = () =>
+ // 防止多次触发显隐
+ leaveFlag = false;
+
+ triggerType = () =>
triggers.reduce(
(map, trigger) => ({
...map,
@@ -122,7 +123,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 +133,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 +184,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,9 +216,35 @@ export default class Popup extends Component {
}
};
+ 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', () => {
+ console.log('====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' });
+ });
+ }
+ }
+
installed() {
this.updatePopper();
- this.updateTrigger();
+ this.addTriggerEvent();
+ this.addPopContentEvent();
+
this.visible = this.props.visible;
}
@@ -240,7 +273,7 @@ export default class Popup extends Component {
updatePopper = () => {
this.popper = 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,
});
};
@@ -287,21 +320,33 @@ 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;
+ });
+
+ console.log('===children', children);
return (
-
- {
- if (e?.detail?.context?.nodeName === 'T-BUTTON') {
- this.clickHandle(e.detail.e);
- }
- }}
- >
- {trigger}
-
+ <>
+ {children.length > 1 ? (
+
+ {children}
+
+ ) : (
+ cloneElement(children[0] as VNode, { ref: this.triggerRef })
+ )}
{this.getVisible() || !props.destroyOnClose ? (
{
)}
) : null}
-
+ >
);
}
}
diff --git a/src/select-input/_example/multiple.tsx b/src/select-input/_example/multiple.tsx
new file mode 100644
index 0000000..b480ddf
--- /dev/null
+++ b/src/select-input/_example/multiple.tsx
@@ -0,0 +1,197 @@
+import 'tdesign-web-components/select-input';
+import 'tdesign-web-components/checkbox';
+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/style/index.js b/src/select-input/style/index.js
index 72b5677..e927dae 100644
--- a/src/select-input/style/index.js
+++ b/src/select-input/style/index.js
@@ -1 +1,9 @@
-import '../../_common/style/web/components/select-input/_index.less';
+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/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 16f4a94..2579c2d 100644
--- a/src/tag-input/tag-input.tsx
+++ b/src/tag-input/tag-input.tsx
@@ -507,7 +507,7 @@ export default class TagInput extends Component {
const suffixIconNode = showClearIcon ? (
Date: Wed, 18 Sep 2024 17:10:11 +0800
Subject: [PATCH 09/15] =?UTF-8?q?feat(popup):=20=E4=BC=98=E5=8C=96popup?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/button/button.tsx | 9 ++-
src/common.ts | 4 ++
src/common/portal.tsx | 4 ++
src/input/input.tsx | 12 +++-
src/popup/_example/placement.tsx | 70 ++++++-------------
src/popup/popup.tsx | 97 +++++++++++++++-----------
src/select-input/SelectInput.tsx | 13 +++-
src/select-input/SelectInputSingle.tsx | 10 +++
src/select-input/_example/single.tsx | 2 +-
9 files changed, 123 insertions(+), 98 deletions(-)
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/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/input/input.tsx b/src/input/input.tsx
index a44e828..bf9d02f 100644
--- a/src/input/input.tsx
+++ b/src/input/input.tsx
@@ -284,12 +284,13 @@ export default class Input extends Component {
render(props: OmiProps) {
const {
+ innerClass,
+ innerStyle,
autoWidth,
placeholder,
disabled,
status,
size,
- className,
prefixIcon,
suffixIcon,
clearable,
@@ -308,11 +309,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.value === undefined ? undefined : String(this.value),
status,
@@ -419,7 +424,7 @@ export default class Input extends Component {
);
const renderInputNode = (