Skip to content

Commit d40540c

Browse files
authored
fix(SelectInput): ensure onBlur can trigger when set popupVisible (#3838)
* fix(SelectInput): ensure `onBlur` can trigger when set `popupVisible` * fix(Cascader): pass `tagInputValue` to onBlur callback when `multiple` * fix(SelectInput): handle onBlur for cases without panel * fix(SelectInput): improve blur handling * chore: clarify comment on blur handling
1 parent 65dbf00 commit d40540c

File tree

5 files changed

+177
-93
lines changed

5 files changed

+177
-93
lines changed

packages/components/cascader/Cascader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ const Cascader: React.FC<CascaderProps> = (originalProps) => {
206206
value: cascaderContext.value,
207207
e: context.e,
208208
inputValue: inputVal,
209+
...(props.multiple ? { tagInputValue: context.tagInputValue } : {}),
209210
});
210211
props?.selectInputProps?.onBlur?.(val, context);
211212
}}

packages/components/select-input/SelectInput.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
1-
import React, { useRef, useImperativeHandle } from 'react';
1+
import React, { useImperativeHandle, useRef } from 'react';
2+
23
import classNames from 'classnames';
4+
35
import useConfig from '../hooks/useConfig';
4-
import Popup, { PopupRef, PopupVisibleChangeContext } from '../popup';
5-
import useSingle from './useSingle';
6+
import useDefaultProps from '../hooks/useDefaultProps';
7+
import Popup, { type PopupRef, type PopupVisibleChangeContext } from '../popup';
8+
import { selectInputDefaultProps } from './defaultProps';
69
import useMultiple from './useMultiple';
710
import useOverlayInnerStyle from './useOverlayInnerStyle';
8-
import { TdSelectInputProps } from './type';
9-
import { StyledProps } from '../common';
10-
import { selectInputDefaultProps } from './defaultProps';
11-
import useDefaultProps from '../hooks/useDefaultProps';
12-
import { InputRef } from '../input';
11+
import useSingle from './useSingle';
12+
13+
import type { StyledProps } from '../common';
14+
import type { InputRef } from '../input';
15+
import type { TdSelectInputProps } from './type';
1316

1417
export interface SelectInputProps extends TdSelectInputProps, StyledProps {
1518
updateScrollTop?: (content: HTMLDivElement) => void;
1619
options?: any[]; // 参数穿透options, 给SelectInput/SelectInput 自定义选中项呈现的内容和多选状态下设置折叠项内容
1720
}
1821

1922
const SelectInput = React.forwardRef<Partial<PopupRef & InputRef>, SelectInputProps>((originalProps, ref) => {
23+
const { classPrefix: prefix } = useConfig();
24+
2025
const props = useDefaultProps<SelectInputProps>(originalProps, selectInputDefaultProps);
26+
const { multiple, value, popupVisible, popupProps, borderless, disabled } = props;
27+
2128
const selectInputRef = useRef<PopupRef>(null);
2229
const selectInputWrapRef = useRef<HTMLDivElement>(null);
23-
const { classPrefix: prefix } = useConfig();
24-
const { multiple, value, popupVisible, popupProps, borderless, disabled } = props;
30+
2531
const { commonInputProps, inputRef, singleInputValue, onInnerClear, renderSelectSingle } = useSingle(props);
26-
const { tagInputRef, multipleInputValue, renderSelectMultiple } = useMultiple(props);
32+
const { tagInputRef, tags, multipleInputValue, renderSelectMultiple } = useMultiple(props);
2733

28-
const { tOverlayInnerStyle, innerPopupVisible, onInnerPopupVisibleChange } = useOverlayInnerStyle(props, {
29-
afterHidePopup: onInnerBlur,
30-
});
34+
const { tOverlayInnerStyle, innerPopupVisible, onInnerPopupVisibleChange, skipNextBlur } = useOverlayInnerStyle(
35+
props,
36+
{
37+
afterHidePopup: onInnerBlur,
38+
},
39+
);
3140

3241
const popupClasses = classNames([
3342
props.className,
@@ -49,11 +58,16 @@ const SelectInput = React.forwardRef<Partial<PopupRef & InputRef>, SelectInputPr
4958
// 浮层显示的受控与非受控
5059
const visibleProps = { visible: popupVisible ?? innerPopupVisible };
5160

52-
// SelectInput.blur is not equal to Input or TagInput, example: click popup panel.
53-
// if trigger blur on click popup panel, filter data of tree select can not be checked.
61+
/* SelectInput 与普通 Input 的 blur 事件触发时机不同
62+
该组件的 blur 事件在 popup 隐藏时才触发,避免点击浮层内容时触发 blur 事件 */
5463
function onInnerBlur(ctx: PopupVisibleChangeContext) {
64+
if (skipNextBlur.current) return;
5565
const inputValue = props.multiple ? multipleInputValue : singleInputValue;
56-
const params: Parameters<TdSelectInputProps['onBlur']>[1] = { e: ctx.e, inputValue };
66+
const params: Parameters<TdSelectInputProps['onBlur']>[1] = {
67+
e: ctx.e,
68+
inputValue,
69+
...(props.multiple && { tagInputValue: tags }),
70+
};
5771
props.onBlur?.(props.value, params);
5872
}
5973

@@ -76,11 +90,12 @@ const SelectInput = React.forwardRef<Partial<PopupRef & InputRef>, SelectInputPr
7690
{multiple
7791
? renderSelectMultiple({
7892
commonInputProps,
79-
onInnerClear,
8093
popupVisible: visibleProps.visible,
8194
allowInput: props.allowInput,
95+
onInnerClear,
96+
onInnerBlur,
8297
})
83-
: renderSelectSingle(visibleProps.visible)}
98+
: renderSelectSingle(visibleProps.visible, onInnerBlur)}
8499
</Popup>
85100
</div>
86101
);

packages/components/select-input/useMultiple.tsx

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import React, { useRef, MouseEvent } from 'react';
2-
import { isObject } from 'lodash-es';
1+
import React, { useRef } from 'react';
2+
33
import classNames from 'classnames';
4-
import { TdSelectInputProps, SelectInputChangeContext, SelectInputKeys, SelectInputValue } from './type';
5-
import TagInput, { TagInputValue } from '../tag-input';
6-
import { SelectInputCommonProperties } from './interface';
7-
import useControlled from '../hooks/useControlled';
4+
import { isObject } from 'lodash-es';
5+
86
import useConfig from '../hooks/useConfig';
9-
import { InputRef } from '../input';
10-
import { StyledProps } from '../common';
7+
import useControlled from '../hooks/useControlled';
8+
import TagInput, { type TagInputValue } from '../tag-input';
9+
10+
import type { StyledProps } from '../common';
11+
import type { InputRef } from '../input';
12+
import type { SelectInputCommonProperties } from './interface';
13+
import type { SelectInputChangeContext, SelectInputKeys, SelectInputValue, TdSelectInputProps } from './type';
1114

1215
export interface RenderSelectMultipleParams {
1316
commonInputProps: SelectInputCommonProperties;
14-
onInnerClear: (context: { e: MouseEvent<SVGElement> }) => void;
1517
popupVisible: boolean;
1618
allowInput: boolean;
19+
onInnerClear: (context: { e: React.MouseEvent<SVGElement> }) => void;
20+
onInnerBlur?: (context: { e: React.FocusEvent<HTMLInputElement> }) => void;
1721
}
1822

1923
const DEFAULT_KEYS = {
@@ -29,8 +33,12 @@ export interface SelectInputProps extends TdSelectInputProps, StyledProps {
2933
export default function useMultiple(props: SelectInputProps) {
3034
const { value } = props;
3135
const { classPrefix } = useConfig();
32-
const tagInputRef = useRef<InputRef>(null);
36+
3337
const [tInputValue, setTInputValue] = useControlled(props, 'inputValue', props.onInputChange);
38+
39+
const tagInputRef = useRef<InputRef>(null);
40+
const blurTimeoutRef = useRef(null);
41+
3442
const iKeys: SelectInputKeys = { ...DEFAULT_KEYS, ...props.keys };
3543

3644
const getTags = () => {
@@ -51,44 +59,72 @@ export default function useMultiple(props: SelectInputProps) {
5159
props.onTagChange?.(val, context);
5260
};
5361

54-
const renderSelectMultiple = (p: RenderSelectMultipleParams) => (
55-
<TagInput
56-
ref={tagInputRef}
57-
{...p.commonInputProps}
58-
autoWidth={props.autoWidth}
59-
readonly={props.readonly}
60-
minCollapsedNum={props.minCollapsedNum}
61-
collapsedItems={props.collapsedItems}
62-
tag={props.tag}
63-
valueDisplay={props.valueDisplay}
64-
placeholder={tPlaceholder}
65-
options={props.options}
66-
value={tags}
67-
inputValue={p.popupVisible && p.allowInput ? tInputValue : ''}
68-
onChange={onTagInputChange}
69-
onInputChange={(val, context) => {
70-
// 筛选器统一特性:筛选器按下回车时不清空输入框
71-
if (context?.trigger === 'enter' || context?.trigger === 'blur') return;
72-
setTInputValue(val, { trigger: context.trigger, e: context.e });
73-
}}
74-
tagProps={props.tagProps}
75-
onClear={p.onInnerClear}
76-
// [Important Info]: SelectInput.blur is not equal to TagInput, example: click popup panel
77-
onFocus={(val, context) => {
78-
props.onFocus?.(props.value, { ...context, tagInputValue: val });
79-
}}
80-
onBlur={!props.panel ? props.onBlur : null}
81-
{...props.tagInputProps}
82-
inputProps={{
83-
...props.inputProps,
84-
readonly: !props.allowInput || props.readonly,
85-
inputClass: classNames(props.tagInputProps?.className, {
86-
[`${classPrefix}-input--focused`]: p.popupVisible,
87-
[`${classPrefix}-is-focused`]: p.popupVisible,
88-
}),
89-
}}
90-
/>
91-
);
62+
const renderSelectMultiple = (p: RenderSelectMultipleParams) => {
63+
const handleBlur = (value: SelectInputValue, context: { e: React.FocusEvent<HTMLInputElement> }) => {
64+
if (blurTimeoutRef.current) {
65+
clearTimeout(blurTimeoutRef.current);
66+
}
67+
// 强制把 popupVisible 设置为 false 时,点击 input,会出现 blur -> focus 的情况,因此忽略前面短暂的 blur 事件
68+
blurTimeoutRef.current = setTimeout(() => {
69+
if (blurTimeoutRef.current) {
70+
if (!p.popupVisible) {
71+
p.onInnerBlur(context);
72+
} else if (!props.panel) {
73+
props.onBlur?.(value, { e: context.e, inputValue: tInputValue, tagInputValue: tags });
74+
}
75+
}
76+
blurTimeoutRef.current = null;
77+
}, 150);
78+
};
79+
80+
const handleFocus = (
81+
val: TagInputValue,
82+
context: { e: React.FocusEvent<HTMLInputElement>; inputValue: string },
83+
) => {
84+
if (blurTimeoutRef.current) {
85+
clearTimeout(blurTimeoutRef.current);
86+
blurTimeoutRef.current = null;
87+
}
88+
props.onFocus?.(props.value, { ...context, tagInputValue: val });
89+
};
90+
91+
return (
92+
<TagInput
93+
ref={tagInputRef}
94+
{...p.commonInputProps}
95+
autoWidth={props.autoWidth}
96+
readonly={props.readonly}
97+
minCollapsedNum={props.minCollapsedNum}
98+
collapsedItems={props.collapsedItems}
99+
tag={props.tag}
100+
valueDisplay={props.valueDisplay}
101+
placeholder={tPlaceholder}
102+
options={props.options}
103+
value={tags}
104+
inputValue={p.popupVisible && p.allowInput ? tInputValue : ''}
105+
onChange={onTagInputChange}
106+
onInputChange={(val, context) => {
107+
// 筛选器统一特性:筛选器按下回车时不清空输入框
108+
if (context?.trigger === 'enter' || context?.trigger === 'blur') return;
109+
setTInputValue(val, { trigger: context.trigger, e: context.e });
110+
}}
111+
tagProps={props.tagProps}
112+
onClear={p.onInnerClear}
113+
// [Important Info]: SelectInput.blur is not equal to TagInput, example: click popup panel
114+
onFocus={handleFocus}
115+
onBlur={handleBlur}
116+
{...props.tagInputProps}
117+
inputProps={{
118+
...props.inputProps,
119+
readonly: !props.allowInput || props.readonly,
120+
inputClass: classNames(props.tagInputProps?.className, {
121+
[`${classPrefix}-input--focused`]: p.popupVisible,
122+
[`${classPrefix}-is-focused`]: p.popupVisible,
123+
}),
124+
}}
125+
/>
126+
);
127+
};
92128

93129
return {
94130
tags,

packages/components/select-input/useOverlayInnerStyle.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React, { useMemo } from 'react';
2-
import { isObject, isFunction } from 'lodash-es';
1+
import { isFunction, isObject } from 'lodash-es';
2+
import React, { useMemo, useRef } from 'react';
33

44
import useControlled from '../hooks/useControlled';
55

6-
import { TdSelectInputProps } from './type';
7-
import { TdPopupProps, PopupVisibleChangeContext } from '../popup';
6+
import type { PopupVisibleChangeContext, TdPopupProps } from '../popup';
7+
import type { TdSelectInputProps } from './type';
88

99
export type overlayStyleProps = Pick<
1010
TdSelectInputProps,
@@ -30,6 +30,8 @@ export default function useOverlayInnerStyle(
3030
const { popupProps, autoWidth, readonly, disabled, onPopupVisibleChange, allowInput } = props;
3131
const [innerPopupVisible, setInnerPopupVisible] = useControlled(props, 'popupVisible', onPopupVisibleChange);
3232

33+
const skipNextBlur = useRef(false);
34+
3335
const matchWidthFunc = (triggerElement: HTMLElement, popupElement: HTMLElement) => {
3436
if (!triggerElement || !popupElement) return;
3537

@@ -69,15 +71,15 @@ export default function useOverlayInnerStyle(
6971
};
7072

7173
const onInnerPopupVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => {
72-
if (disabled || readonly) {
73-
return;
74-
}
74+
skipNextBlur.current = false;
75+
if (disabled || readonly) return;
7576
// 如果点击触发元素(输入框)且为可输入状态,则继续显示下拉框
7677
const newVisible = context.trigger === 'trigger-element-click' && allowInput ? true : visible;
7778
if (props.popupVisible !== newVisible) {
7879
setInnerPopupVisible(newVisible, context);
7980
if (!newVisible) {
8081
extra?.afterHidePopup?.(context);
82+
skipNextBlur.current = true;
8183
}
8284
}
8385
};
@@ -97,6 +99,7 @@ export default function useOverlayInnerStyle(
9799
return {
98100
tOverlayInnerStyle,
99101
innerPopupVisible,
102+
skipNextBlur,
100103
onInnerPopupVisibleChange,
101104
};
102105
}

0 commit comments

Comments
 (0)