Skip to content

Commit 89426ce

Browse files
authored
feat(dropdown.select): supports uncontrolled and interactive optimization (#484)
* feat: supports uncontrolled and interactive optimization * fix: value priority is greater than defaultValue
1 parent 6b80b7c commit 89426ce

File tree

5 files changed

+88
-55
lines changed

5 files changed

+88
-55
lines changed

src/dropdown/__tests__/dropdown.test.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ describe('Test Dropdown.Select Component', () => {
3434
expect(asFragment()).toMatchSnapshot();
3535
});
3636

37+
it('Should support defaultValue', () => {
38+
const fn = jest.fn();
39+
const { getByTestId } = render(
40+
<Dropdown.Select
41+
defaultValue={[2, 3]}
42+
options={new Array(10).fill('').map((_, idx) => idx)}
43+
onChange={fn}
44+
getPopupContainer={(node) => node}
45+
>
46+
<Button type="link" data-testid="trigger">
47+
打开下拉
48+
</Button>
49+
</Dropdown.Select>
50+
);
51+
fireEvent.click(getByTestId('trigger'));
52+
fireEvent.click(getByTestId('trigger'));
53+
expect(fn).toBeCalledWith([2, 3]);
54+
});
55+
3756
it('Should enable virtual list', () => {
3857
const { container, getByTestId } = render(
3958
<Dropdown.Select
@@ -140,6 +159,7 @@ describe('Test Dropdown.Select Component', () => {
140159
});
141160
// 全选
142161
fireEvent.click(getByText('全选'));
162+
fireEvent.click(getByText('确 定'));
143163
expect(fn).toBeCalledWith([1, 2]);
144164

145165
rerender(
@@ -163,6 +183,7 @@ describe('Test Dropdown.Select Component', () => {
163183
jest.runAllTimers();
164184
});
165185
fireEvent.click(getByText('全选'));
186+
fireEvent.click(getByText('确 定'));
166187
// 取消全选不会取消禁用项的选择
167188
expect(fn).lastCalledWith([2]);
168189

@@ -185,6 +206,7 @@ describe('Test Dropdown.Select Component', () => {
185206
});
186207
// 选中全部
187208
fireEvent.click(getByText('全选'));
209+
fireEvent.click(getByText('确 定'));
188210
expect(fn).lastCalledWith(['Bob', 'Jack']);
189211
});
190212

@@ -228,7 +250,7 @@ describe('Test Dropdown.Select Component', () => {
228250
expect(shadow?.className).not.toContain('active');
229251
});
230252

231-
it('Should call submit when hide', () => {
253+
it('Should call change when hide', () => {
232254
const fn = jest.fn();
233255
const { getByTestId, getByText } = render(
234256
<Dropdown.Select
@@ -237,8 +259,7 @@ describe('Test Dropdown.Select Component', () => {
237259
{ label: '选项一', value: 1 },
238260
{ label: '选项二', value: 2, disabled: true },
239261
]}
240-
onChange={jest.fn()}
241-
onSubmit={fn}
262+
onChange={fn}
242263
getPopupContainer={(node) => node}
243264
>
244265
<Button type="link" data-testid="trigger">
@@ -252,7 +273,7 @@ describe('Test Dropdown.Select Component', () => {
252273
jest.runAllTimers();
253274
});
254275

255-
fireEvent.click(getByText('确 定').parentElement!);
276+
fireEvent.click(getByText('确 定')!);
256277
expect(fn).toBeCalledWith([2]);
257278
expect(fn).toBeCalledTimes(1);
258279

@@ -376,9 +397,11 @@ describe('Test Dropdown.Select Component', () => {
376397
});
377398

378399
fireEvent.click(getByText('1'));
379-
expect(fn).toBeCalledWith([1000, 2, 2000, 1]);
400+
fireEvent.click(getByText('确 定'));
401+
expect(fn).toBeCalledWith([2, 1000, 2000, 1]);
380402

381403
fireEvent.click(getByText('2'));
382-
expect(fn).toBeCalledWith([1000, 2000]);
404+
fireEvent.click(getByText('确 定'));
405+
expect(fn).toBeCalledWith([1000, 2000, 1]);
383406
});
384407
});

src/dropdown/demos/submit.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ export default () => {
2121
{ label: '选项一', value: 1 },
2222
{ label: '选项二', value: 2, disabled: true },
2323
]}
24-
onChange={(checked) => setSelected(checked as number[])}
25-
onSubmit={fetchData}
24+
onChange={(checked) => {
25+
setSelected(checked as number[]);
26+
fetchData();
27+
}}
2628
>
2729
<Button type="link">打开下拉</Button>
2830
</Dropdown.Select>

src/dropdown/demos/virtual.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ export default () => {
1313
console.log(val);
1414
setSelected(val as number[]);
1515
}}
16-
onSubmit={() => {
17-
console.log('submit');
18-
}}
1916
>
2017
<Button type="link">10000 条数据</Button>
2118
</Dropdown.Select>

src/dropdown/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ demo:
2323
| 参数 | 说明 | 类型 | 默认值 |
2424
| ----------------- | ---------------------------------- | ------------------------------------------- | ------ |
2525
| value | 当前选中的值 | `(string \| number)[]` | - |
26+
| defaultValue | 初始值 | `(string \| number)[]` | - |
2627
| className | - | `string` | - |
2728
| options | Checkbox 指定可选项 | `(string \| number \| Option)[]` | `[]` |
2829
| getPopupContainer | 同 Dropdown 的 `getPopupContainer` | `(triggerNode: HTMLElement) => HTMLElement` | - |
2930
| onChange | 变化时的回调函数 | `(checkedValue) => void` | - |
30-
| onSubmit | 提交时的回调函数 | `(checkedValue) => void` | - |

src/dropdown/select.tsx

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import React, { ReactNode, useState } from 'react';
1+
import React, { ReactNode, 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';
4-
import type { CheckboxGroupProps, CheckboxValueType } from 'antd/lib/checkbox/Group';
4+
import type {
5+
CheckboxGroupProps,
6+
CheckboxOptionType,
7+
CheckboxValueType,
8+
} from 'antd/lib/checkbox/Group';
59
import classNames from 'classnames';
610
import { isEqual } from 'lodash';
711
import List from 'rc-virtual-list';
@@ -10,10 +14,9 @@ import './style.scss';
1014

1115
interface IDropdownSelectProps
1216
extends Pick<DropDownProps, 'getPopupContainer'>,
13-
Required<Pick<CheckboxGroupProps, 'value' | 'options' | 'onChange'>> {
17+
Pick<CheckboxGroupProps, 'value' | 'defaultValue' | 'options' | 'onChange'> {
1418
children: ReactNode;
1519
className?: string;
16-
onSubmit?: (value: CheckboxValueType[]) => void;
1720
}
1821

1922
const prefix = 'dtc-dropdown-select';
@@ -23,37 +26,37 @@ const MAX_HEIGHT = 264;
2326
export default function Select({
2427
className,
2528
value,
29+
defaultValue,
2630
options: rawOptions,
2731
children,
2832
getPopupContainer,
2933
onChange,
30-
onSubmit,
3134
}: IDropdownSelectProps) {
3235
const [visible, setVisible] = useState(false);
36+
const [selected, setSelected] = useState<CheckboxValueType[]>(value || defaultValue || []);
3337

3438
const handleCheckedAll = (e: CheckboxChangeEvent) => {
3539
if (e.target.checked) {
36-
onChange?.(options?.map((i) => i.value) || []);
40+
setSelected(options?.map((i) => i.value) || []);
3741
} else {
3842
handleReset();
3943
}
4044
};
4145

4246
const handleSubmit = () => {
43-
onSubmit?.(value);
47+
onChange?.(selected);
4448
setVisible(false);
4549
};
4650

4751
const handleReset = () => {
4852
// Clear checked but disabled item
49-
onChange?.(options?.filter((i) => i.disabled).map((i) => i.value) || []);
53+
setSelected(disabledValue);
5054
};
5155

5256
const handleChange = (e: CheckboxChangeEvent) => {
53-
const next = e.target.checked
54-
? [...(value || []), e.target.value]
55-
: value?.filter((i) => i !== e.target.value);
56-
onChange?.(next);
57+
const { checked, value } = e.target;
58+
const next = checked ? [...selected, value] : selected?.filter((i) => i !== value);
59+
setSelected(next);
5760
};
5861

5962
const handleShadow = (target: HTMLDivElement) => {
@@ -64,51 +67,59 @@ export default function Select({
6467
target.insertBefore(shadow, target.firstChild);
6568
}
6669

67-
if (
68-
Number(
69-
target
70-
.querySelector<HTMLDivElement>('.rc-virtual-list-scrollbar-thumb')
71-
?.style.top.replace('px', '')
72-
) > 0
73-
) {
74-
target.querySelector<HTMLDivElement>(`.${prefix}__shadow`)?.classList.add('active');
70+
const scrollbar_thumb = target.querySelector<HTMLDivElement>(
71+
'.rc-virtual-list-scrollbar-thumb'
72+
);
73+
const shadow = target.querySelector<HTMLDivElement>(`.${prefix}__shadow`);
74+
75+
if (parseFloat(scrollbar_thumb?.style.top as string) > 0) {
76+
shadow?.classList.add('active');
7577
} else {
76-
target
77-
.querySelector<HTMLDivElement>(`.${prefix}__shadow`)
78-
?.classList.remove('active');
78+
shadow?.classList.remove('active');
7979
}
8080
}
8181
};
8282

83-
// Always turn string and number options into complex options
84-
const options = rawOptions.map((i) => {
85-
if (typeof i === 'string' || typeof i === 'number') {
86-
return {
87-
label: i,
88-
value: i,
89-
};
83+
useEffect(() => {
84+
if (value !== undefined && value !== selected) {
85+
setSelected(value || []);
9086
}
87+
}, [value]);
88+
89+
// Always turn string and number options into complex options
90+
const options = useMemo<CheckboxOptionType[]>(() => {
91+
return (
92+
rawOptions?.map((i) => {
93+
if (typeof i === 'string' || typeof i === 'number') {
94+
return {
95+
label: i,
96+
value: i,
97+
};
98+
}
9199

92-
return i;
93-
});
100+
return i;
101+
}) || []
102+
);
103+
}, [rawOptions]);
94104

95-
const resetDisabled = value.every((i) =>
96-
options
97-
?.filter((i) => i.disabled)
98-
.map((i) => i.value)
99-
?.includes(i)
100-
);
105+
const disabledValue = useMemo<CheckboxValueType[]>(() => {
106+
return options?.filter((i) => i.disabled).map((i) => i.value) || [];
107+
}, [options]);
108+
109+
const resetDisabled = selected.every((i) => disabledValue?.includes(i));
101110

102111
// If options' number is larger then the maxHeight, then enable virtual list
103112
const virtual = options.length > Math.floor(MAX_HEIGHT / ITEM_HEIGHT);
104113

105114
// ONLY the options are all be pushed into value array means select all
106-
const checkAll = !!value?.length && isEqual(options.map((i) => i.value).sort(), value.sort());
115+
const checkAll =
116+
!!selected?.length && isEqual(options.map((i) => i.value).sort(), [...selected].sort());
117+
107118
// At least one option's value is included in value array but not all options means indeterminate select
108119
const indeterminate =
109-
!!value?.length &&
110-
!isEqual(options.map((i) => i.value).sort(), value.sort()) &&
111-
options.some((o) => value.includes(o.value));
120+
!!selected?.length &&
121+
!isEqual(options.map((i) => i.value).sort(), [...selected].sort()) &&
122+
options.some((o) => selected.includes(o.value));
112123

113124
const overlay = (
114125
<>
@@ -123,7 +134,7 @@ export default function Select({
123134
</Checkbox>
124135
</Col>
125136
<Col span={24} className={`${prefix}__menu`}>
126-
<Checkbox.Group value={value}>
137+
<Checkbox.Group value={selected}>
127138
<List
128139
data={options}
129140
itemKey="value"

0 commit comments

Comments
 (0)