Skip to content

Commit b7eff27

Browse files
committed
fix: add compatibility for older DOM structures
1 parent bd018e4 commit b7eff27

4 files changed

Lines changed: 201 additions & 169 deletions

File tree

packages/components/input/Input.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ const Input = forwardRefWithStatics(
166166
) : null;
167167

168168
const updateInputWidth = () => {
169-
if (!autoWidth || !inputRef.current || !inputPreRef.current) return;
169+
if (!inputRef.current) return;
170+
if (!autoWidth || !inputPreRef.current) {
171+
// autoWidth 关闭时清除之前设置的内联宽度,避免残留
172+
inputRef.current.style.width = '';
173+
return;
174+
}
170175
const { offsetWidth } = inputPreRef.current;
171176
const { width } = inputPreRef.current.getBoundingClientRect();
172177
// 异步渲染场景下 getBoundingClientRect 宽度为 0,需要使用 offsetWidth

packages/components/select-input/useSingle.tsx

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ export default function useSingle(props: SelectInputProps) {
5858
const blurTimeoutRef = useRef(null);
5959
const customElementRef = useRef<HTMLSpanElement>(null);
6060

61+
// 以下三个状态仅在 allowInput=true 时有意义
6162
const [isTyping, setIsTyping] = useState<boolean>(false);
6263
const [labelWidth, setLabelWidth] = useState<number>(0);
6364
const [suffixSpace, setSuffixSpace] = useState<number>(0);
6465

6566
const singleValueDisplay = useMemo(() => valueDisplay ?? getOptionLabel(value, keys), [value, valueDisplay, keys]);
6667

68+
// allowInput=true 时 DOM 结构采用 absolute 覆盖层实现
6769
const showCustomElement = useMemo(
68-
() => !isTyping && !inputValue && React.isValidElement(singleValueDisplay),
69-
[isTyping, inputValue, singleValueDisplay],
70+
() => allowInput && !isTyping && !inputValue && React.isValidElement(singleValueDisplay),
71+
[allowInput, isTyping, inputValue, singleValueDisplay],
7072
);
7173

7274
const onInnerClear = (context: { e: React.MouseEvent<SVGSVGElement> }) => {
@@ -82,14 +84,16 @@ export default function useSingle(props: SelectInputProps) {
8284
};
8385

8486
useEffect(() => {
87+
if (!allowInput) return;
8588
const labelEl = inputRef.current?.currentElement.querySelector(`.${classPrefix}-input__prefix`);
8689
if (labelEl) {
8790
const prefixWidth = labelEl.getBoundingClientRect().width;
8891
setLabelWidth(prefixWidth);
8992
}
90-
}, [label, classPrefix]);
93+
}, [allowInput, label, classPrefix]);
9194

9295
useEffect(() => {
96+
if (!allowInput) return;
9397
const inputEl = inputRef.current?.inputElement;
9498
if (!inputEl) return;
9599
// autoWidth 且存在自定义元素时需要撑开宽度
@@ -98,32 +102,16 @@ export default function useSingle(props: SelectInputProps) {
98102
return;
99103
}
100104
const el = customElementRef.current;
101-
102-
const measure = () => {
103-
// 测量真实内容宽度时,临时强制 nowrap,避免被父级容器(受 suffixSpace 影响)压缩换行导致测量值偏小
104-
const prevWhiteSpace = el.style.whiteSpace;
105-
el.style.whiteSpace = 'nowrap';
106-
const { width } = el.getBoundingClientRect();
107-
el.style.whiteSpace = prevWhiteSpace;
108-
inputEl.style.minWidth = width > 0 ? `${width}px` : '';
109-
};
110-
111-
measure();
112-
113-
let ro: ResizeObserver | null = null;
114-
if (typeof ResizeObserver !== 'undefined') {
115-
ro = new ResizeObserver(() => {
116-
measure();
117-
});
118-
ro.observe(el);
119-
}
120-
121-
return () => {
122-
ro?.disconnect();
123-
};
124-
}, [autoWidth, showCustomElement, singleValueDisplay]);
105+
// 测量真实内容宽度时,临时强制 nowrap,避免被父级容器(受 suffixSpace 影响)压缩换行导致测量值偏小
106+
const prevWhiteSpace = el.style.whiteSpace;
107+
el.style.whiteSpace = 'nowrap';
108+
const { width } = el.getBoundingClientRect();
109+
el.style.whiteSpace = prevWhiteSpace;
110+
inputEl.style.minWidth = width > 0 ? `${width}px` : '';
111+
}, [allowInput, autoWidth, showCustomElement, singleValueDisplay]);
125112

126113
useEffect(() => {
114+
if (!allowInput) return;
127115
// 避免内容延伸盖到右侧的 suffixIcon 区域,需要测量 input 右侧到 wrapper 右侧的距离作为 right 留白
128116
if (!showCustomElement) {
129117
setSuffixSpace(0);
@@ -149,7 +137,7 @@ export default function useSingle(props: SelectInputProps) {
149137
wrapperEl.removeEventListener('mouseenter', measure);
150138
wrapperEl.removeEventListener('mouseleave', measure);
151139
};
152-
}, [showCustomElement, singleValueDisplay, clearable, suffixIcon, props.suffix]);
140+
}, [allowInput, showCustomElement, singleValueDisplay, clearable, suffixIcon, props.suffix]);
153141

154142
const renderSelectSingle = (
155143
popupVisible: boolean,
@@ -182,7 +170,54 @@ export default function useSingle(props: SelectInputProps) {
182170
// !popupVisible && setInputValue(getInputValue(value, keys), { ...context, trigger: 'input' });
183171
};
184172

173+
const sharedInputProps = {
174+
ref: inputRef,
175+
...commonInputProps,
176+
autocomplete: 'off' as const,
177+
autoWidth,
178+
style: inputProps?.style,
179+
onChange: onInnerInputChange,
180+
onClear: onInnerClear,
181+
onFocus: handleFocus,
182+
onEnter: (val, context) => {
183+
props.onEnter?.(value, { ...context, inputValue: val });
184+
},
185+
onBlur: handleBlur,
186+
...inputProps,
187+
inputClass: classNames(inputProps?.inputClass, {
188+
[`${classPrefix}-input--focused`]: popupVisible,
189+
[`${classPrefix}-is-focused`]: popupVisible,
190+
}),
191+
};
192+
193+
// allowInput=false 且 singleValueDisplay 是 React 元素,直接放进 label,不需要 absolute
194+
// (历史实现,避免 DOM 结构变更,保留原有逻辑,大版本可考虑彻底统一)
195+
const isStaticCustomElement = !allowInput && React.isValidElement(singleValueDisplay);
196+
197+
if (isStaticCustomElement) {
198+
return (
199+
<Input
200+
{...sharedInputProps}
201+
showClearIconOnEmpty={clearable}
202+
allowInput={false}
203+
label={
204+
(label || singleValueDisplay) && (
205+
<>
206+
{label}
207+
{singleValueDisplay as React.ReactNode}
208+
</>
209+
)
210+
}
211+
value=" "
212+
placeholder=""
213+
/>
214+
);
215+
}
216+
185217
const displayedValue = (): string => {
218+
if (inputProps?.value !== undefined) {
219+
return inputProps.value;
220+
}
186221
if (popupVisible && inputValue) {
187222
return inputValue;
188223
}
@@ -200,7 +235,7 @@ export default function useSingle(props: SelectInputProps) {
200235
return singleValueDisplay;
201236
}
202237
if (showCustomElement) return '';
203-
return props.placeholder;
238+
return inputProps?.placeholder ?? props.placeholder;
204239
};
205240

206241
const labelNode = showCustomElement ? (
@@ -233,11 +268,9 @@ export default function useSingle(props: SelectInputProps) {
233268

234269
return (
235270
<Input
236-
ref={inputRef}
271+
{...sharedInputProps}
237272
// 当 valueDisplay 为 自定义元素时,选中内容时 input 依旧为空,确保此时 clear icon 可见
238273
showClearIconOnEmpty={clearable && showCustomElement}
239-
{...commonInputProps}
240-
autocomplete="off"
241274
suffix={
242275
labelNode ||
243276
(commonInputProps.suffix && (
@@ -247,22 +280,10 @@ export default function useSingle(props: SelectInputProps) {
247280
</>
248281
))
249282
}
250-
autoWidth={autoWidth}
251-
style={inputProps?.style}
252283
allowInput={allowInput}
253284
label={label}
254285
value={displayedValue()}
255286
placeholder={displayedPlaceholder()}
256-
onChange={onInnerInputChange}
257-
onClear={onInnerClear}
258-
// [Important Info]: SelectInput.blur is not equal to Input, example: click popup panel
259-
onFocus={handleFocus}
260-
onEnter={(val, context) => {
261-
props.onEnter?.(value, { ...context, inputValue: val });
262-
}}
263-
// onBlur need to triggered by input when popup panel is null or when popupVisible is forced to false
264-
onBlur={handleBlur}
265-
{...inputProps}
266287
onCompositionstart={(v, ctx) => {
267288
setIsTyping(true);
268289
inputProps?.onCompositionstart?.(v, ctx);
@@ -271,10 +292,6 @@ export default function useSingle(props: SelectInputProps) {
271292
setIsTyping(false);
272293
inputProps?.onCompositionend?.(v, ctx);
273294
}}
274-
inputClass={classNames(inputProps?.inputClass, {
275-
[`${classPrefix}-input--focused`]: popupVisible,
276-
[`${classPrefix}-is-focused`]: popupVisible,
277-
})}
278295
/>
279296
);
280297
};

0 commit comments

Comments
 (0)