Skip to content

Commit 632cb70

Browse files
committed
chore: complete keyboard control
1 parent 39e7c31 commit 632cb70

File tree

3 files changed

+120
-72
lines changed

3 files changed

+120
-72
lines changed

packages/components/select/base/Select.tsx

Lines changed: 72 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,76 @@ const Select = forwardRefWithStatics(
119119
reserveKeyword,
120120
);
121121

122+
const onCheckAllChange = useCallback(
123+
(checkAll: boolean, e: React.MouseEvent<HTMLLIElement>) => {
124+
const isDisabledCheckAll = (opt: TdOptionProps) => opt.checkAll && opt.disabled;
125+
if (!multiple || currentOptions.some((opt) => !isSelectOptionGroup(opt) && isDisabledCheckAll(opt))) {
126+
return;
127+
}
128+
129+
const { valueKey } = getKeyMapping(keys);
130+
const isObjectType = valueType === 'object';
131+
132+
const enabledOptions: SelectOption[] = [];
133+
134+
currentOptions.forEach((option) => {
135+
// 如果涉及分组,需要将分组内的选项进行计算,否则会影响全选的功能
136+
if (isSelectOptionGroup(option)) {
137+
option.children?.forEach((item) => {
138+
if (!item.checkAll && !item.disabled) {
139+
enabledOptions.push(item);
140+
}
141+
});
142+
} else {
143+
!option.checkAll && !option.disabled && enabledOptions.push(option);
144+
}
145+
});
146+
147+
const currentValues = Array.isArray(value) ? value : [];
148+
const disabledSelectedOptions: SelectOption[] = [];
149+
150+
const isDisabledAndSelected = (opt: TdOptionProps) => {
151+
if (opt.checkAll || !opt.disabled) return false;
152+
if (isObjectType) return currentValues.some((v) => get(v, valueKey) === opt[valueKey]);
153+
return currentValues.includes(opt[valueKey]);
154+
};
155+
156+
currentOptions.forEach((opt) => {
157+
if (isSelectOptionGroup(opt)) {
158+
// 处理分组内的禁用选项
159+
opt.children?.forEach((item) => {
160+
if (isDisabledAndSelected(item)) {
161+
disabledSelectedOptions.push(item);
162+
}
163+
});
164+
} else if (isDisabledAndSelected(opt)) {
165+
disabledSelectedOptions.push(opt);
166+
}
167+
});
168+
169+
let checkAllValue: SelectValue[];
170+
171+
if (checkAll) {
172+
// 全选:选中所有未禁用的选项 + 保留已选中的禁用选项
173+
const enabledValues = enabledOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
174+
const disabledValues = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
175+
checkAllValue = [...disabledValues, ...enabledValues];
176+
} else {
177+
// 取消全选:只保留已选中的禁用选项
178+
checkAllValue = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
179+
}
180+
181+
const { currentSelectedOptions } = getSelectedOptions(checkAllValue, multiple, valueType, keys, valueToOption);
182+
183+
onChange?.(checkAllValue, {
184+
e,
185+
trigger: checkAll ? 'check' : 'uncheck',
186+
selectedOptions: currentSelectedOptions,
187+
});
188+
},
189+
[currentOptions, keys, multiple, onChange, value, valueToOption, valueType],
190+
);
191+
122192
const { handleKeyDown, hoverIndex } = useKeyboardControl({
123193
displayOptions: currentOptions as TdOptionProps[],
124194
max,
@@ -127,6 +197,8 @@ const Select = forwardRefWithStatics(
127197
innerPopupVisible: showPopup,
128198
setInnerValue: onChange,
129199
innerValue: value,
200+
onCheckAllChange,
201+
selectInputRef,
130202
});
131203

132204
const selectedLabel = useMemo(() => {
@@ -191,73 +263,6 @@ const Select = forwardRefWithStatics(
191263
}
192264
};
193265

194-
const onCheckAllChange = (checkAll: boolean, e: React.MouseEvent<HTMLLIElement>) => {
195-
const isDisabledCheckAll = (opt: TdOptionProps) => opt.checkAll && opt.disabled;
196-
if (!multiple || currentOptions.some((opt) => !isSelectOptionGroup(opt) && isDisabledCheckAll(opt))) {
197-
return;
198-
}
199-
200-
const { valueKey } = getKeyMapping(keys);
201-
const isObjectType = valueType === 'object';
202-
203-
const enabledOptions: SelectOption[] = [];
204-
205-
currentOptions.forEach((option) => {
206-
// 如果涉及分组,需要将分组内的选项进行计算,否则会影响全选的功能
207-
if (isSelectOptionGroup(option)) {
208-
option.children?.forEach((item) => {
209-
if (!item.checkAll && !item.disabled) {
210-
enabledOptions.push(item);
211-
}
212-
});
213-
} else {
214-
!option.checkAll && !option.disabled && enabledOptions.push(option);
215-
}
216-
});
217-
218-
const currentValues = Array.isArray(value) ? value : [];
219-
const disabledSelectedOptions: SelectOption[] = [];
220-
221-
const isDisabledAndSelected = (opt: TdOptionProps) => {
222-
if (opt.checkAll || !opt.disabled) return false;
223-
if (isObjectType) return currentValues.some((v) => get(v, valueKey) === opt[valueKey]);
224-
return currentValues.includes(opt[valueKey]);
225-
};
226-
227-
currentOptions.forEach((opt) => {
228-
if (isSelectOptionGroup(opt)) {
229-
// 处理分组内的禁用选项
230-
opt.children?.forEach((item) => {
231-
if (isDisabledAndSelected(item)) {
232-
disabledSelectedOptions.push(item);
233-
}
234-
});
235-
} else if (isDisabledAndSelected(opt)) {
236-
disabledSelectedOptions.push(opt);
237-
}
238-
});
239-
240-
let checkAllValue: SelectValue[];
241-
242-
if (checkAll) {
243-
// 全选:选中所有未禁用的选项 + 保留已选中的禁用选项
244-
const enabledValues = enabledOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
245-
const disabledValues = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
246-
checkAllValue = [...disabledValues, ...enabledValues];
247-
} else {
248-
// 取消全选:只保留已选中的禁用选项
249-
checkAllValue = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
250-
}
251-
252-
const { currentSelectedOptions } = getSelectedOptions(checkAllValue, multiple, valueType, keys, valueToOption);
253-
254-
onChange?.(checkAllValue, {
255-
e,
256-
trigger: checkAll ? 'check' : 'uncheck',
257-
selectedOptions: currentSelectedOptions,
258-
});
259-
};
260-
261266
// 选中 Popup 某项
262267
const handleChange = (
263268
value: string | number | Array<string | number | Record<string, string | number>>,

packages/components/select/hooks/useKeyboardControl.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { useState, KeyboardEvent } from 'react';
2-
import type { SelectOption, TdOptionProps, SelectValue } from '../type';
1+
import { useState, KeyboardEvent, useRef, useEffect } from 'react';
2+
3+
import useConfig from '../../hooks/useConfig';
34
import { getSelectValueArr } from '../util/helper';
45

6+
import type { SelectOption, TdOptionProps, SelectValue } from '../type';
7+
58
export type useKeyboardControlType = {
69
displayOptions: TdOptionProps[];
710
innerPopupVisible: boolean;
811
setInnerPopupVisible: any;
12+
onCheckAllChange: (checkAll: boolean, e?: React.MouseEvent<HTMLLIElement>) => void;
913
setInnerValue: Function;
1014
innerValue: SelectValue<SelectOption>;
1115
multiple: boolean;
1216
max: number;
17+
selectInputRef: any;
1318
};
1419
export default function useKeyboardControl({
1520
displayOptions,
@@ -19,10 +24,36 @@ export default function useKeyboardControl({
1924
innerValue,
2025
multiple,
2126
max,
27+
onCheckAllChange,
28+
selectInputRef,
2229
}: useKeyboardControlType) {
2330
const [hoverIndex, changeHoverIndex] = useState(-1);
2431
const filteredOptions = useState([]); // 处理普通场景选项过滤键盘选中的问题
2532
const virtualFilteredOptions = useState([]); // 处理虚拟滚动下选项过滤通过键盘选择的问题
33+
const { classPrefix } = useConfig();
34+
// 全选判断
35+
const isCheckAll = useRef(false);
36+
useEffect(() => {
37+
if (!Array.isArray(innerValue)) return;
38+
isCheckAll.current = innerValue.length === displayOptions.filter((v) => !(v.disabled || v.checkAll)).length;
39+
}, [innerValue, displayOptions]);
40+
41+
const handleKeyboardScroll = (hoverIndex: number) => {
42+
const popupContent = selectInputRef.current.getPopupContentElement();
43+
44+
const optionSelector = `.${classPrefix}-select-option`;
45+
const selector = `.${classPrefix}-select-option__hover`;
46+
const firstSelectedNode: HTMLDivElement = popupContent.querySelector(selector);
47+
if (firstSelectedNode) {
48+
// 小于0时不需要特殊处理,会被设为0
49+
const scrollHeight = popupContent.querySelector(optionSelector).clientHeight * hoverIndex;
50+
51+
popupContent.scrollTo({
52+
top: scrollHeight,
53+
behavior: 'smooth',
54+
});
55+
}
56+
};
2657

2758
const handleKeyDown = (_value, { e }: { e: KeyboardEvent }) => {
2859
const optionsListLength = displayOptions.length;
@@ -43,6 +74,7 @@ export default function useKeyboardControl({
4374
newIndex -= 1;
4475
}
4576
changeHoverIndex(newIndex);
77+
handleKeyboardScroll(newIndex);
4678
break;
4779
case 'ArrowDown':
4880
e.preventDefault();
@@ -56,6 +88,7 @@ export default function useKeyboardControl({
5688
newIndex += 1;
5789
}
5890
changeHoverIndex(newIndex);
91+
handleKeyboardScroll(newIndex);
5992
break;
6093
case 'Enter':
6194
if (hoverIndex === -1) break;
@@ -78,7 +111,7 @@ export default function useKeyboardControl({
78111
if (hoverIndex === -1) return;
79112

80113
if (displayOptions[hoverIndex].checkAll) {
81-
// onCheckAllChange(!isCheckAll);
114+
onCheckAllChange(!isCheckAll.current);
82115
return;
83116
}
84117

packages/components/tag-input/TagInput.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ const TagInput = forwardRef<InputRef, TagInputProps>((originalProps, ref) => {
121121
props.onClear?.({ e });
122122
};
123123

124+
const onKeydown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
125+
onInputBackspaceKeyDown(value, context);
126+
inputProps.onKeydown?.(value, context);
127+
};
128+
129+
const onKeyup = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
130+
onInputBackspaceKeyUp(value);
131+
inputProps.onKeyup?.(value, context);
132+
};
133+
124134
const suffixIconNode = showClearIcon ? (
125135
<CloseCircleFilledIcon className={CLEAR_CLASS} onClick={onClearClick} />
126136
) : (
@@ -192,8 +202,6 @@ const TagInput = forwardRef<InputRef, TagInputProps>((originalProps, ref) => {
192202
onPaste={onPaste}
193203
onClick={onInnerClick}
194204
onEnter={onInputEnter}
195-
onKeydown={onInputBackspaceKeyDown}
196-
onKeyup={onInputBackspaceKeyUp}
197205
onMouseenter={(context) => {
198206
addHover(context);
199207
scrollToRightOnEnter();
@@ -214,6 +222,8 @@ const TagInput = forwardRef<InputRef, TagInputProps>((originalProps, ref) => {
214222
onCompositionstart={onInputCompositionstart}
215223
onCompositionend={onInputCompositionend}
216224
{...inputProps}
225+
onKeydown={onKeydown}
226+
onKeyup={onKeyup}
217227
/>
218228
);
219229
});

0 commit comments

Comments
 (0)