Skip to content

Commit 767427c

Browse files
committed
fix: support group
1 parent 09c1bbb commit 767427c

File tree

8 files changed

+406
-222
lines changed

8 files changed

+406
-222
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
interface OptionGroup<T> {
4+
group?: string;
5+
children?: T[];
6+
}
7+
8+
interface KeyboardProps<T> {
9+
options: (T | OptionGroup<T>)[];
10+
initialIndex: number;
11+
onSelect: (option: T, e: React.KeyboardEvent) => void;
12+
showPopup?: boolean;
13+
}
14+
15+
const isOptionGroup = <T>(option: T | OptionGroup<T>): option is OptionGroup<T> =>
16+
option && typeof option === 'object' && 'group' in option && 'children' in option;
17+
18+
const flattenOptions = <T>(options: (T | OptionGroup<T>)[]): T[] => {
19+
const flattened: T[] = [];
20+
options.forEach((option) => {
21+
if (isOptionGroup(option)) {
22+
if (option.children) {
23+
flattened.push(...option.children);
24+
}
25+
} else {
26+
flattened.push(option);
27+
}
28+
});
29+
return flattened;
30+
};
31+
32+
const useKeyboard = <T>({ options, initialIndex, onSelect, showPopup }: KeyboardProps<T>) => {
33+
const [hoverIndex, setHoverIndex] = useState(initialIndex);
34+
35+
const flatOptions = flattenOptions(options);
36+
37+
useEffect(() => {
38+
console.log('useKeyboard effect:', {
39+
initialIndex,
40+
showPopup,
41+
optionsLength: flatOptions.length,
42+
});
43+
setHoverIndex(initialIndex);
44+
}, [flatOptions.length, initialIndex, showPopup]);
45+
46+
const findNextEnabledIndex = useCallback(
47+
(startIndex: number, direction: 1 | -1) => {
48+
if (!flatOptions || flatOptions.length === 0) return -1;
49+
const len = flatOptions.length;
50+
let i = startIndex;
51+
for (let step = 0; step < len; step += 1) {
52+
i = direction === 1 ? (i + 1) % len : (i - 1 + len) % len;
53+
const opt = flatOptions[i];
54+
// @ts-ignore
55+
if (!opt?.disabled) return i;
56+
}
57+
return startIndex;
58+
},
59+
[flatOptions],
60+
);
61+
62+
const handleKeyDown = useCallback(
63+
(e: React.KeyboardEvent) => {
64+
if (flatOptions.length === 0) return;
65+
66+
switch (e.key) {
67+
case 'ArrowDown':
68+
e.preventDefault();
69+
setHoverIndex((prev) => {
70+
const start = prev < 0 ? -1 : prev;
71+
const next = findNextEnabledIndex(start, 1);
72+
return next === start ? prev : next;
73+
});
74+
break;
75+
76+
case 'ArrowUp':
77+
e.preventDefault();
78+
setHoverIndex((prev) => {
79+
const start = prev < 0 ? 0 : prev;
80+
const next = findNextEnabledIndex(start, -1);
81+
return next === start ? prev : next;
82+
});
83+
break;
84+
85+
case 'Enter':
86+
e.preventDefault();
87+
if (hoverIndex >= 0 && hoverIndex < flatOptions.length) {
88+
const current = flatOptions[hoverIndex];
89+
onSelect(current, e);
90+
}
91+
break;
92+
93+
default:
94+
break;
95+
}
96+
},
97+
[flatOptions, hoverIndex, onSelect, findNextEnabledIndex],
98+
);
99+
100+
return {
101+
hoverIndex,
102+
handleKeyDown,
103+
};
104+
};
105+
106+
export default useKeyboard;

packages/components/hooks/useKeyboardNavigation.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

packages/components/select/_example/group.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const OptionGroupSelect = () => {
4949

5050
return (
5151
<Space breakLine style={{ width: '800px' }}>
52-
<Select value={value} onChange={onChange} style={{ width: '40%' }} options={groupOptions} filterable />
52+
{/* <Select value={value} onChange={onChange} style={{ width: '40%' }} options={groupOptions} filterable /> */}
5353
<Select value={value2} onChange={onChange2} style={{ width: '40%' }} multiple filterable>
5454
<Option value="all" label="全选" checkAll></Option>
5555
<OptionGroup label="分组一" divider={true}>

packages/components/select/base/Option.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
1+
import React, { useEffect, useMemo } from 'react';
22
import classNames from 'classnames';
33
import { get, isNumber, isString } from 'lodash-es';
44

55
import useConfig from '../../hooks/useConfig';
66
import useDomRefCallback from '../../hooks/useDomRefCallback';
77
import useRipple from '../../hooks/useRipple';
8+
import { isAllSelected } from '../util/checkAll';
89
import { getKeyMapping } from '../util/helper';
910

1011
import type { StyledProps } from '../../common';
@@ -34,6 +35,9 @@ export interface SelectOptionProps
3435
optionLength?: number;
3536
isVirtual?: boolean;
3637
onRowMounted?: (rowData: { ref: HTMLElement; data: SelectOption }) => void;
38+
isHovered?: boolean;
39+
currentOptions?: SelectOption[];
40+
valueType?: TdSelectProps['valueType'];
3741
}
3842

3943
const componentType = 'select';
@@ -57,29 +61,26 @@ const Option: React.FC<SelectOptionProps> = (props) => {
5761
style,
5862
className,
5963
isVirtual,
64+
isHovered,
65+
currentOptions,
66+
valueType,
6067
} = props;
6168

6269
const label = propLabel || value;
6370
const disabled = propDisabled || (multiple && Array.isArray(selectedValue) && max && selectedValue.length >= max);
64-
const initCheckedStatus = !(Array.isArray(selectedValue) && selectedValue.length === props.optionLength);
6571

6672
let selected: boolean;
6773
let indeterminate: boolean;
68-
// 处理存在禁用项时,全选状态无法来回切换的问题
69-
const [allSelectableChecked, setAllSelectableChecked] = useState(initCheckedStatus);
7074

7175
const titleContent = useMemo(() => {
72-
// 外部设置 props,说明希望受控
7376
const controlledTitle = Reflect.has(props, 'title');
7477
if (controlledTitle) return propTitle;
7578
if (typeof label === 'string') return label;
7679
return null;
77-
// eslint-disable-next-line react-hooks/exhaustive-deps
78-
}, [propTitle, label]);
80+
}, [propTitle, label, props]);
7981

8082
const { classPrefix } = useConfig();
8183

82-
// 使用斜八角动画
8384
const [optionRef, setRefCurrent] = useDomRefCallback();
8485
useRipple(optionRef);
8586

@@ -106,7 +107,6 @@ const Option: React.FC<SelectOptionProps> = (props) => {
106107
if (multiple && Array.isArray(selectedValue)) {
107108
selected = selectedValue.some((item) => {
108109
if (isNumber(item) || isString(item)) {
109-
// 如果非 object 类型
110110
return item === value;
111111
}
112112
return get(item, valueKey) === value;
@@ -121,9 +121,10 @@ const Option: React.FC<SelectOptionProps> = (props) => {
121121
if (!disabled && !checkAll) {
122122
onSelect(value, { label: String(label), selected, event, restData });
123123
}
124-
if (checkAll) {
125-
props.onCheckAllChange?.(allSelectableChecked, event);
126-
setAllSelectableChecked(!allSelectableChecked);
124+
if (checkAll && currentOptions) {
125+
// 使用工具函数计算当前是否全选
126+
const allSelected = isAllSelected(currentOptions, selectedValue, keys, valueType);
127+
props.onCheckAllChange?.(!allSelected, event);
127128
}
128129
};
129130

@@ -161,7 +162,7 @@ const Option: React.FC<SelectOptionProps> = (props) => {
161162
<li
162163
className={classNames(className, `${classPrefix}-${componentType}-option`, {
163164
[`${classPrefix}-is-disabled`]: disabled,
164-
[`${classPrefix}-is-selected`]: selected,
165+
[`${classPrefix}-is-selected`]: selected && !isHovered,
165166
[`${classPrefix}-size-s`]: size === 'small',
166167
[`${classPrefix}-size-l`]: size === 'large',
167168
})}

0 commit comments

Comments
 (0)