Skip to content

Commit 368e331

Browse files
authored
fix(dropdown): optimize Dropdown.Select (#635)
1 parent db846be commit 368e331

3 files changed

Lines changed: 98 additions & 60 deletions

File tree

src/dropdown/__tests__/__snapshots__/dropdown.test.tsx.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,9 @@ exports[`Test Dropdown.Select Component Should match snapshot 1`] = `
2828
class="ant-checkbox-wrapper"
2929
>
3030
<span
31-
class="ant-checkbox ant-checkbox-indeterminate"
31+
class="ant-checkbox"
3232
>
3333
<input
34-
aria-checked="mixed"
3534
class="ant-checkbox-input"
3635
type="checkbox"
3736
value=""

src/dropdown/__tests__/dropdown.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ describe('Test Dropdown.Select Component', () => {
307307
act(() => {
308308
jest.runAllTimers();
309309
});
310-
// Should be indeterminate
311-
expect(getByText('全选').previousElementSibling?.className).toContain(
310+
// Only disabled item selected → not indeterminate (no enabled items selected)
311+
expect(getByText('全选').previousElementSibling?.className).not.toContain(
312312
'ant-checkbox-indeterminate'
313313
);
314314

@@ -347,9 +347,9 @@ describe('Test Dropdown.Select Component', () => {
347347
</Button>
348348
</Dropdown.Select>
349349
);
350-
// Should be indeterminate
350+
// All enabled items selected → checked
351351
expect(getByText('全选').previousElementSibling?.className).toContain(
352-
'ant-checkbox-indeterminate'
352+
'ant-checkbox-checked'
353353
);
354354

355355
rerender(

src/dropdown/select.tsx

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
1+
import React, { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
22
import { Button, Checkbox, Col, Dropdown, type DropDownProps, Row, Space } from 'antd';
33
import type { CheckboxChangeEvent } from 'antd/lib/checkbox';
44
import type {
@@ -7,7 +7,6 @@ import type {
77
CheckboxValueType,
88
} from 'antd/lib/checkbox/Group';
99
import classNames from 'classnames';
10-
import { isEqual } from 'lodash-es';
1110
import List from 'rc-virtual-list';
1211

1312
import useLocale from '../locale/useLocale';
@@ -38,24 +37,102 @@ export default function Select({
3837

3938
const locale = useLocale('Dropdown');
4039

41-
const handleCheckedAll = (e: CheckboxChangeEvent) => {
42-
if (e.target.checked) {
43-
setSelected(options?.map((i) => i.value) || []);
44-
} else {
45-
handleReset();
40+
useEffect(() => {
41+
if (value !== undefined) {
42+
setSelected(value);
4643
}
47-
};
44+
}, [value]);
45+
46+
// Always turn string and number options into complex options
47+
const options = useMemo<CheckboxOptionType[]>(() => {
48+
return (
49+
rawOptions?.map((i) => {
50+
if (typeof i === 'string' || typeof i === 'number') {
51+
return {
52+
label: i,
53+
value: i,
54+
};
55+
}
56+
57+
return i;
58+
}) || []
59+
);
60+
}, [rawOptions]);
61+
62+
/**
63+
* The "derived metadata" of the selected data
64+
* It does not directly participate in rendering but is only used for logical judgment
65+
*
66+
* Purpose:
67+
* - Clearly distinguish enabled / disabled
68+
* - Prevent disabled items from being accidentally selected / reset
69+
*/
70+
const selectionMeta = useMemo(() => {
71+
const enabled = new Set<CheckboxValueType>();
72+
const disabled = new Set<CheckboxValueType>();
73+
74+
options.forEach((o) => {
75+
if (o.disabled) {
76+
disabled.add(o.value);
77+
} else {
78+
enabled.add(o.value);
79+
}
80+
});
81+
82+
const selectedEnabled: CheckboxValueType[] = [];
83+
const selectedDisabled: CheckboxValueType[] = [];
84+
85+
selected.forEach((v) => {
86+
if (enabled.has(v)) {
87+
selectedEnabled.push(v);
88+
}
89+
if (disabled.has(v)) {
90+
selectedDisabled.push(v);
91+
}
92+
});
93+
94+
return {
95+
/** All selectable (non-disabled) values */
96+
enabledValues: Array.from(enabled),
97+
/** All disabled values */
98+
disabledValues: Array.from(disabled),
99+
/** Currently selected enabled items */
100+
selectedEnabled,
101+
/** Currently selected disabled items (for reset retention) */
102+
selectedDisabled,
103+
/** All enabled items are selected */
104+
checkAll: enabled.size > 0 && selectedEnabled.length === enabled.size,
105+
/** Partial enabled items are selected */
106+
indeterminate: selectedEnabled.length > 0 && selectedEnabled.length < enabled.size,
107+
/**
108+
* Whether to disable the Reset button
109+
* Only disabled when:
110+
* - All currently selected items are disabled
111+
*/
112+
resetDisabled: selected.length > 0 && selected.every((v) => disabled.has(v)),
113+
};
114+
}, [options, selected]);
115+
116+
const handleReset = useCallback(() => {
117+
setSelected(selectionMeta.selectedDisabled);
118+
}, [selectionMeta.selectedDisabled]);
119+
120+
const handleCheckedAll = useCallback(
121+
(e: CheckboxChangeEvent) => {
122+
if (e.target.checked) {
123+
setSelected([...selectionMeta.enabledValues, ...selectionMeta.selectedDisabled]);
124+
} else {
125+
handleReset();
126+
}
127+
},
128+
[selectionMeta, handleReset]
129+
);
48130

49131
const handleSubmit = () => {
50132
onChange?.(selected);
51133
setVisible(false);
52134
};
53135

54-
const handleReset = () => {
55-
// Clear checked but disabled item
56-
setSelected(disabledValue);
57-
};
58-
59136
const handleChange = (e: CheckboxChangeEvent) => {
60137
const { checked, value } = e.target;
61138
const next = checked ? [...selected, value] : selected?.filter((i) => i !== value);
@@ -83,55 +160,17 @@ export default function Select({
83160
}
84161
};
85162

86-
useEffect(() => {
87-
if (value !== undefined && value !== selected) {
88-
setSelected(value || []);
89-
}
90-
}, [value]);
91-
92-
// Always turn string and number options into complex options
93-
const options = useMemo<CheckboxOptionType[]>(() => {
94-
return (
95-
rawOptions?.map((i) => {
96-
if (typeof i === 'string' || typeof i === 'number') {
97-
return {
98-
label: i,
99-
value: i,
100-
};
101-
}
102-
103-
return i;
104-
}) || []
105-
);
106-
}, [rawOptions]);
107-
108-
const disabledValue = useMemo<CheckboxValueType[]>(() => {
109-
return options?.filter((i) => i.disabled).map((i) => i.value) || [];
110-
}, [options]);
111-
112-
const resetDisabled = selected.every((i) => disabledValue?.includes(i));
113-
114163
// If options' number is larger then the maxHeight, then enable virtual list
115164
const virtual = options.length > Math.floor(MAX_HEIGHT / ITEM_HEIGHT);
116165

117-
// ONLY the options are all be pushed into value array means select all
118-
const checkAll =
119-
!!selected?.length && isEqual(options.map((i) => i.value).sort(), [...selected].sort());
120-
121-
// At least one option's value is included in value array but not all options means indeterminate select
122-
const indeterminate =
123-
!!selected?.length &&
124-
!isEqual(options.map((i) => i.value).sort(), [...selected].sort()) &&
125-
options.some((o) => selected.includes(o.value));
126-
127166
const overlay = (
128167
<>
129168
<Row>
130169
<Col span={24} className={`${prefix}__col`}>
131170
<Checkbox
132171
onChange={handleCheckedAll}
133-
checked={checkAll}
134-
indeterminate={indeterminate}
172+
checked={selectionMeta.checkAll}
173+
indeterminate={selectionMeta.indeterminate}
135174
>
136175
{locale.selectAll}
137176
</Checkbox>
@@ -171,7 +210,7 @@ export default function Select({
171210
</Col>
172211
</Row>
173212
<Space size={8} className={`${prefix}__btns`}>
174-
<Button size="small" disabled={resetDisabled} onClick={handleReset}>
213+
<Button size="small" disabled={selectionMeta.resetDisabled} onClick={handleReset}>
175214
{locale.resetText}
176215
</Button>
177216
<Button size="small" type="primary" onClick={handleSubmit}>

0 commit comments

Comments
 (0)