Skip to content
15 changes: 0 additions & 15 deletions packages/components/_util/composeRefs.ts

This file was deleted.

18 changes: 14 additions & 4 deletions packages/components/_util/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { canUseDocument } from './dom';

export const on = ((): any => {
if (canUseDocument && document.addEventListener) {
return (element: Node, event: string, handler: EventListenerOrEventListenerObject): any => {
return (
element: Node,
event: string,
handler: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions,
): any => {
if (element && event && handler) {
element.addEventListener(event, handler, false);
element.addEventListener(event, handler, options ?? false);
}
};
}
Expand All @@ -17,9 +22,14 @@ export const on = ((): any => {

export const off = ((): any => {
if (canUseDocument && document.removeEventListener) {
return (element: Node, event: string, handler: EventListenerOrEventListenerObject): any => {
return (
element: Node,
event: string,
handler: EventListenerOrEventListenerObject,
options?: EventListenerOptions,
): any => {
if (element && event) {
element.removeEventListener(event, handler, false);
element.removeEventListener(event, handler, options ?? false);
}
};
}
Expand Down
14 changes: 14 additions & 0 deletions packages/components/_util/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,17 @@ export const getNodeRef: <T = any>(node: React.ReactNode) => React.Ref<T> | null
}
return null;
};

export function composeRefs<T>(...refs: React.Ref<T>[]) {
return (instance: T) => {
// eslint-disable-next-line no-restricted-syntax
for (const ref of refs) {
if (typeof ref === 'function') {
ref(instance);
} else if (ref) {
(ref as any).current = instance;
}
}
};
}

2 changes: 1 addition & 1 deletion packages/components/alert/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useLocaleReceiver } from '../locale/LocalReceiver';
import { TdAlertProps } from './type';
import { StyledProps } from '../common';
import { alertDefaultProps } from './defaultProps';
import composeRefs from '../_util/composeRefs';
import { composeRefs } from '../_util/ref';
import useDefaultProps from '../hooks/useDefaultProps';

const transitionTime = 200;
Expand Down
2 changes: 1 addition & 1 deletion packages/components/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from 'classnames';
import useConfig from '../hooks/useConfig';
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
import useCommonClassName from '../hooks/useCommonClassName';
import composeRefs from '../_util/composeRefs';
import { composeRefs } from '../_util/ref';
import { TdAvatarProps } from './type';
import { StyledProps } from '../common';
import AvatarContext from './AvatarContext';
Expand Down
26 changes: 15 additions & 11 deletions packages/components/cascader/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { useState, useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import { isArray, isEqual, isFunction } from 'lodash-es';

import TreeStore from '@tdesign/common-js/tree-v1/tree-store';
import type { TypeTreeNodeData } from '@tdesign/common-js/tree-v1/types';
import { getTreeValue, getCascaderValue, isEmptyValues, isValueInvalid } from './core/helper';
import { treeNodesEffect, treeStoreExpendEffect } from './core/effect';

import useControlled from '../hooks/useControlled';
import useInnerPopupVisible from '../hooks/useInnerPopupVisible';
import { treeNodesEffect, treeStoreExpendEffect } from './core/effect';
import { getCascaderValue, getTreeValue, isEmptyValues, isValueInvalid } from './core/helper';

import type { TreeOptionData } from '../common';
import type {
TreeNode,
TreeNodeValue,
TdCascaderProps,
TreeNodeModel,
CascaderChangeSource,
CascaderValue,
TdCascaderProps,
TreeNode,
TreeNodeModel,
TreeNodeValue,
} from './interface';

import { TreeOptionData } from '../common';

export const useCascaderContext = (props: TdCascaderProps) => {
const [innerValue, setInnerValue] = useControlled(props, 'value', props.onChange);
const [innerPopupVisible, setPopupVisible] = useControlled(props, 'popupVisible', props.onPopupVisibleChange);
Expand All @@ -30,6 +30,10 @@ export const useCascaderContext = (props: TdCascaderProps) => {
const [expend, setExpend] = useState<TreeNodeValue[]>([]);
const [scopeVal, setScopeVal] = useState(undefined);

const handlePopupVisibleChange = useInnerPopupVisible((v, ctx) => {
setPopupVisible(v, ctx);
});

const cascaderContext = useMemo(() => {
const {
size,
Expand Down Expand Up @@ -65,14 +69,14 @@ export const useCascaderContext = (props: TdCascaderProps) => {
setInnerValue(val, { source, node });
},
visible: innerPopupVisible,
setVisible: setPopupVisible,
setVisible: handlePopupVisibleChange,
treeNodes,
setTreeNodes,
inputVal,
setInputVal,
setExpend,
};
}, [props, scopeVal, innerPopupVisible, treeStore, treeNodes, inputVal, setInnerValue, setPopupVisible]);
}, [props, scopeVal, innerPopupVisible, treeStore, treeNodes, inputVal, setInnerValue, handlePopupVisibleChange]);

const isFilterable = useMemo(
() => Boolean(props.filterable || isFunction(props.filter)),
Expand Down
38 changes: 21 additions & 17 deletions packages/components/date-picker/hooks/useRange.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { CalendarIcon as TdCalendarIcon } from 'tdesign-icons-react';
import classNames from 'classnames';
import { isValidDate, formatDate, getDefaultFormat, parseToDayjs } from '@tdesign/common-js/date-picker/format';

import { formatDate, getDefaultFormat, isValidDate, parseToDayjs } from '@tdesign/common-js/date-picker/format';
import useConfig from '../../hooks/useConfig';
import useGlobalIcon from '../../hooks/useGlobalIcon';
import { RangeInputRefInterface } from '../../range-input';
import { TdDateRangePickerProps, DateValue } from '../type';
import useInnerPopupVisible from '../../hooks/useInnerPopupVisible';
import useRangeValue from './useRangeValue';

import type { TdPopupProps } from '../../popup/type';
import type { RangeInputRefInterface } from '../../range-input';
import type { DateValue, TdDateRangePickerProps } from '../type';

export const PARTIAL_MAP = { first: 'start', second: 'end' };

Expand Down Expand Up @@ -123,26 +126,27 @@ export default function useRange(props: TdDateRangePickerProps) {
},
};

const handleInnerVisibleChange = useInnerPopupVisible((visible, context) => {
if (props.disabled) return;
// 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理
props.popupProps?.onVisibleChange?.(visible, context);
// 输入框点击不关闭面板
if (context.trigger === 'trigger-element-mousedown') {
const indexMap = { 0: 'first', 1: 'second' };
inputRef.current.focus({ position: indexMap[activeIndex] });
return setPopupVisible(true);
}
setPopupVisible(visible);
});

// popup 设置
const popupProps = {
expandAnimation: true,
...props.popupProps,
trigger: 'mousedown' as TdPopupProps['trigger'],
overlayInnerStyle: props.popupProps?.overlayInnerStyle ?? { width: 'auto' },
overlayClassName: classNames(props.popupProps?.overlayClassName, `${name}__panel-container`),
onVisibleChange: (visible: boolean, context) => {
if (props.disabled) return;
// 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理
props.popupProps?.onVisibleChange?.(visible, context);
// 输入框点击不关闭面板
if (context.trigger === 'trigger-element-mousedown') {
const indexMap = { 0: 'first', 1: 'second' };
inputRef.current.focus({ position: indexMap[activeIndex] });
return setPopupVisible(true);
}

setPopupVisible(visible);
},
onVisibleChange: handleInnerVisibleChange,
};

// 输入框响应 value 变化
Expand Down
29 changes: 17 additions & 12 deletions packages/components/date-picker/hooks/useSingle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import { CalendarIcon as TdCalendarIcon } from 'tdesign-icons-react';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { omit } from 'lodash-es';
import React, { useEffect, useRef, useState } from 'react';
import { CalendarIcon as TdCalendarIcon } from 'tdesign-icons-react';

import {
formatDate,
formatTime,
Expand All @@ -12,9 +13,11 @@ import {
} from '@tdesign/common-js/date-picker/format';
import useConfig from '../../hooks/useConfig';
import useGlobalIcon from '../../hooks/useGlobalIcon';
import useInnerPopupVisible from '../../hooks/useInnerPopupVisible';
import useSingleValue from './useSingleValue';

import type { TdPopupProps } from '../../popup/type';
import type { TdDatePickerProps } from '../type';
import useSingleValue from './useSingleValue';

export default function useSingleInput(props: TdDatePickerProps) {
const { classPrefix, datePicker: globalDatePickerConfig } = useConfig();
Expand Down Expand Up @@ -105,22 +108,24 @@ export default function useSingleInput(props: TdDatePickerProps) {
},
};

const handleInnerVisibleChange = useInnerPopupVisible((visible, context) => {
if (props.disabled) return;
// 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理
props.popupProps?.onVisibleChange?.(visible, context);
if (context.trigger === 'trigger-element-mousedown') {
return setPopupVisible(true);
}
setPopupVisible(visible);
});

// popup 设置
let popupProps = {
expandAnimation: true,
...props.popupProps,
trigger: 'mousedown' as TdPopupProps['trigger'],
overlayInnerStyle: props.popupProps?.overlayInnerStyle ?? { width: 'auto' },
overlayClassName: classNames(props.popupProps?.overlayClassName, `${name}__panel-container`),
onVisibleChange: (visible: boolean, context: any) => {
if (props.disabled) return;
// 这里劫持了进一步向 popup 传递的 onVisibleChange 事件,为了保证可以在 Datepicker 中使用 popupProps.onVisibleChange,故此处理
props.popupProps?.onVisibleChange?.(visible, context);
if (context.trigger === 'trigger-element-mousedown') {
return setPopupVisible(true);
}
setPopupVisible(visible);
},
onVisibleChange: handleInnerVisibleChange,
};

// tag-input 设置
Expand Down
25 changes: 25 additions & 0 deletions packages/components/hooks/useInnerPopupVisible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import useConfig from './useConfig';
import type { PopupVisibleChangeContext } from '../popup';

function useInnerPopupVisible(handler: (visible: boolean, ctx: PopupVisibleChangeContext) => void) {
const { classPrefix } = useConfig();

const isClearIconClick = (target: EventTarget | null): boolean => {
if (!target || !(target instanceof Element)) return false;
return !!(
target?.closest?.(`.${classPrefix}-input__suffix-clear`) ||
target?.closest?.(`.${classPrefix}-tag-input__suffix-clear`) ||
target?.closest?.(`.${classPrefix}-range-input__suffix-clear`)
);
};

return (visible: boolean, ctx: PopupVisibleChangeContext) => {
// Fix: https://github.com/Tencent/tdesign-react/issues/2320
// 点击 clear icon 是否触发 Popup 显隐切换的逻辑交给具体逻辑自己处理,避免重复触发
if (isClearIconClick(ctx?.e?.target) && visible) return;
// 执行原函数
handler(visible, ctx);
};
}

export default useInnerPopupVisible;
35 changes: 12 additions & 23 deletions packages/components/input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import React, { useState, useRef, useImperativeHandle, useEffect } from 'react';
import classNames from 'classnames';
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import {
BrowseIcon as TdBrowseIcon,
BrowseOffIcon as TdBrowseOffIcon,
CloseCircleFilledIcon as TdCloseCircleFilledIcon,
} from 'tdesign-icons-react';
import classNames from 'classnames';
import { isFunction } from 'lodash-es';
import useLayoutEffect from '../hooks/useLayoutEffect';

import forwardRefWithStatics from '../_util/forwardRefWithStatics';
import parseTNode from '../_util/parseTNode';
import useConfig from '../hooks/useConfig';
import useGlobalIcon from '../hooks/useGlobalIcon';
import { TdInputProps } from './type';
import { StyledProps, TNode, TElement } from '../common';
import InputGroup from './InputGroup';
import useControlled from '../hooks/useControlled';
import useDefaultProps from '../hooks/useDefaultProps';
import useGlobalIcon from '../hooks/useGlobalIcon';
import useLayoutEffect from '../hooks/useLayoutEffect';
import { useLocaleReceiver } from '../locale/LocalReceiver';
import { inputDefaultProps } from './defaultProps';
import parseTNode from '../_util/parseTNode';
import InputGroup from './InputGroup';
import useLengthLimit from './useLengthLimit';
import useDefaultProps from '../hooks/useDefaultProps';

import type { StyledProps, TElement, TNode } from '../common';
import type { TdInputProps } from './type';

export interface InputProps extends TdInputProps, StyledProps {
showInput?: boolean; // 控制透传readonly同时是否展示input 默认保留 因为正常Input需要撑开宽度
Expand Down Expand Up @@ -136,13 +138,7 @@ const Input = forwardRefWithStatics(
let suffixIconNew = suffixIcon;

if (isShowClearIcon)
suffixIconNew = (
<CloseCircleFilledIcon
className={`${classPrefix}-input__suffix-clear`}
onMouseDown={handleMouseDown}
onClick={handleClear}
/>
);
suffixIconNew = <CloseCircleFilledIcon className={`${classPrefix}-input__suffix-clear`} onClick={handleClear} />;
if (type === 'password' && typeof suffixIcon === 'undefined') {
if (renderType === 'password') {
suffixIconNew = (
Expand Down Expand Up @@ -316,13 +312,6 @@ const Input = forwardRefWithStatics(
onChange(newStr, { e, trigger });
}
}
// 添加MouseDown阻止冒泡,防止點擊Clear value會導致彈窗閃爍一下
// https://github.com/Tencent/tdesign-react/issues/2320
function handleMouseDown(e: React.MouseEvent<SVGSVGElement, globalThis.MouseEvent>) {
e.stopPropagation();
// 兼容React16
e.nativeEvent.stopImmediatePropagation();
}
function handleClear(e: React.MouseEvent<SVGSVGElement>) {
onChange?.('', { e, trigger: 'clear' });
onClear?.({ e });
Expand Down
Loading
Loading