Skip to content

Commit d8a13f3

Browse files
authored
feat: Select support maxCount (#1012)
* feat: Select support maxCount * fix: fix * demo: update demo * test: fix test case * docs: update docs * fix: fix * fix: fix * fix: fix * test: add test case * test: fix case
1 parent 56fd0d9 commit d8a13f3

File tree

8 files changed

+105
-37
lines changed

8 files changed

+105
-37
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export default () => (
130130
| virtual | Disable virtual scroll | boolean | true |
131131
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
132132
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
133+
| maxCount | The max number of items can be selected | number | - |
133134

134135
### Methods
135136

docs/demo/multiple-with-maxCount.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: multiple-with-maxCount
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/multiple-with-maxCount.tsx"></code>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import Select from 'rc-select';
4+
import '../../assets/index.less';
5+
6+
const Test: React.FC = () => {
7+
const [value, setValue] = React.useState<string[]>(['1']);
8+
9+
const onChange = (v: any) => {
10+
setValue(v);
11+
};
12+
13+
return (
14+
<>
15+
<h2>Multiple with maxCount</h2>
16+
<Select
17+
maxCount={4}
18+
mode="multiple"
19+
value={value}
20+
animation="slide-up"
21+
choiceTransitionName="rc-select-selection__choice-zoom"
22+
style={{ width: 500 }}
23+
optionFilterProp="children"
24+
optionLabelProp="children"
25+
placeholder="please select"
26+
onChange={onChange}
27+
options={Array.from({ length: 20 }, (_, i) => ({
28+
label: <span>中文{i}</span>,
29+
value: i.toString(),
30+
}))}
31+
/>
32+
</>
33+
);
34+
};
35+
36+
export default Test;

src/OptionList.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
4545
onPopupScroll,
4646
} = useBaseProps();
4747
const {
48+
maxCount,
4849
flattenOptions,
4950
onActiveValue,
5051
defaultActiveFirstOption,
@@ -70,6 +71,11 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
7071
// =========================== List ===========================
7172
const listRef = React.useRef<ListRef>(null);
7273

74+
const overMaxCount = React.useMemo<boolean>(
75+
() => multiple && typeof maxCount !== 'undefined' && rawValues.size >= maxCount,
76+
[multiple, maxCount, rawValues.size],
77+
);
78+
7379
const onListMouseDown: React.MouseEventHandler<HTMLDivElement> = (event) => {
7480
event.preventDefault();
7581
};
@@ -87,9 +93,9 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
8793
for (let i = 0; i < len; i += 1) {
8894
const current = (index + i * offset + len) % len;
8995

90-
const { group, data } = memoFlattenOptions[current];
96+
const { group, data } = memoFlattenOptions[current] || {};
9197

92-
if (!group && !data.disabled) {
98+
if (!group && !data?.disabled && !overMaxCount) {
9399
return current;
94100
}
95101
}
@@ -198,7 +204,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
198204
case KeyCode.ENTER: {
199205
// value
200206
const item = memoFlattenOptions[activeIndex];
201-
if (item && !item.data.disabled) {
207+
if (item && !item?.data?.disabled && !overMaxCount) {
202208
onSelectValue(item.value);
203209
} else {
204210
onSelectValue(undefined);
@@ -256,8 +262,9 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
256262

257263
const renderItem = (index: number) => {
258264
const item = memoFlattenOptions[index];
259-
if (!item) return null;
260-
265+
if (!item) {
266+
return null;
267+
}
261268
const itemData = item.data || {};
262269
const { value } = itemData;
263270
const { group } = item;
@@ -327,11 +334,13 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
327334
// Option
328335
const selected = isSelected(value);
329336

337+
const mergedDisabled = disabled || (!selected && overMaxCount);
338+
330339
const optionPrefixCls = `${itemPrefixCls}-option`;
331340
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, className, {
332341
[`${optionPrefixCls}-grouped`]: groupOption,
333-
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
334-
[`${optionPrefixCls}-disabled`]: disabled,
342+
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
343+
[`${optionPrefixCls}-disabled`]: mergedDisabled,
335344
[`${optionPrefixCls}-selected`]: selected,
336345
});
337346

@@ -356,13 +365,13 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
356365
className={optionClassName}
357366
title={optionTitle}
358367
onMouseMove={() => {
359-
if (activeIndex === itemIndex || disabled) {
368+
if (activeIndex === itemIndex || mergedDisabled) {
360369
return;
361370
}
362371
setActive(itemIndex);
363372
}}
364373
onClick={() => {
365-
if (!disabled) {
374+
if (!mergedDisabled) {
366375
onSelectValue(value);
367376
}
368377
}}
@@ -380,7 +389,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
380389
customizeIcon={menuItemSelectedIcon}
381390
customizeIconProps={{
382391
value,
383-
disabled,
392+
disabled: mergedDisabled,
384393
isSelected: selected,
385394
}}
386395
>

src/Select.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import OptGroup from './OptGroup';
4545
import Option from './Option';
4646
import OptionList from './OptionList';
4747
import SelectContext from './SelectContext';
48+
import type { SelectContextProps } from './SelectContext';
4849
import useCache from './hooks/useCache';
4950
import useFilterOptions from './hooks/useFilterOptions';
5051
import useId from './hooks/useId';
@@ -156,6 +157,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
156157
labelInValue?: boolean;
157158
value?: ValueType | null;
158159
defaultValue?: ValueType | null;
160+
maxCount?: number;
159161
onChange?: (value: ValueType, option: OptionType | OptionType[]) => void;
160162
}
161163

@@ -203,6 +205,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
203205
defaultValue,
204206
labelInValue,
205207
onChange,
208+
maxCount,
206209

207210
...restProps
208211
} = props;
@@ -596,7 +599,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
596599
};
597600

598601
// ========================== Context ===========================
599-
const selectContext = React.useMemo(() => {
602+
const selectContext = React.useMemo<SelectContextProps>(() => {
600603
const realVirtual = virtual !== false && dropdownMatchSelectWidth !== false;
601604
return {
602605
...parsedOptions,
@@ -612,9 +615,11 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
612615
listHeight,
613616
listItemHeight,
614617
childrenAsData,
618+
maxCount,
615619
optionRender,
616620
};
617621
}, [
622+
maxCount,
618623
parsedOptions,
619624
displayOptions,
620625
onActiveValue,
@@ -625,6 +630,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
625630
mergedFieldNames,
626631
virtual,
627632
dropdownMatchSelectWidth,
633+
direction,
628634
listHeight,
629635
listItemHeight,
630636
childrenAsData,

src/SelectContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface SelectContextProps {
2525
listHeight?: number;
2626
listItemHeight?: number;
2727
childrenAsData?: boolean;
28+
maxCount?: number;
2829
}
2930

3031
const SelectContext = React.createContext<SelectContextProps>(null);

tests/Multiple.test.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -640,15 +640,10 @@ describe('Select.Multiple', () => {
640640
});
641641
});
642642

643-
describe("autoClearSearchValue", () => {
643+
describe('autoClearSearchValue', () => {
644644
it('search value should not show when autoClearSearchValue is undefined', () => {
645645
const wrapper = mount(
646-
<Select
647-
mode="multiple"
648-
open={false}
649-
showSearch={true}
650-
searchValue="test"
651-
/>,
646+
<Select mode="multiple" open={false} showSearch={true} searchValue="test" />,
652647
);
653648
expect(wrapper.find('input').props().value).toBe('');
654649
});
@@ -666,12 +661,12 @@ describe('Select.Multiple', () => {
666661
});
667662
it('search value should no clear when autoClearSearchValue is false', () => {
668663
const wrapper = mount(
669-
<Select
670-
mode="multiple"
671-
autoClearSearchValue={false}
672-
showSearch={true}
673-
searchValue="test"
674-
/>,
664+
<Select
665+
mode="multiple"
666+
autoClearSearchValue={false}
667+
showSearch={true}
668+
searchValue="test"
669+
/>,
675670
);
676671

677672
toggleOpen(wrapper);
@@ -680,12 +675,7 @@ describe('Select.Multiple', () => {
680675
});
681676
it('search value should clear when autoClearSearchValue is true', () => {
682677
const wrapper = mount(
683-
<Select
684-
mode="multiple"
685-
autoClearSearchValue={true}
686-
showSearch={true}
687-
searchValue="test"
688-
/>,
678+
<Select mode="multiple" autoClearSearchValue={true} showSearch={true} searchValue="test" />,
689679
);
690680
toggleOpen(wrapper);
691681
toggleOpen(wrapper);

tests/Select.test.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ describe('Select.Basic', () => {
653653
});
654654

655655
describe('click input will trigger focus', () => {
656-
let handleFocus;
656+
let handleFocus: jest.Mock;
657657
let wrapper;
658658
beforeEach(() => {
659659
jest.useFakeTimers();
@@ -690,15 +690,15 @@ describe('Select.Basic', () => {
690690
});
691691

692692
it('focus input when placeholder is clicked', () => {
693-
const wrapper = mount(
693+
const selectWrapper = mount(
694694
<Select placeholder="xxxx">
695695
<Option value="1">1</Option>
696696
<Option value="2">2</Option>
697697
</Select>,
698698
);
699-
const inputSpy = jest.spyOn(wrapper.find('input').instance(), 'focus' as any);
700-
wrapper.find('.rc-select-selection-placeholder').simulate('mousedown');
701-
wrapper.find('.rc-select-selection-placeholder').simulate('click');
699+
const inputSpy = jest.spyOn(selectWrapper.find('input').instance(), 'focus' as any);
700+
selectWrapper.find('.rc-select-selection-placeholder').simulate('mousedown');
701+
selectWrapper.find('.rc-select-selection-placeholder').simulate('click');
702702
expect(inputSpy).toHaveBeenCalled();
703703
});
704704
});
@@ -1499,7 +1499,7 @@ describe('Select.Basic', () => {
14991499
);
15001500
expect(menuItemSelectedIcon).toHaveBeenCalledWith({
15011501
value: '1',
1502-
disabled: undefined,
1502+
disabled: false,
15031503
isSelected: true,
15041504
});
15051505

@@ -2105,7 +2105,7 @@ describe('Select.Basic', () => {
21052105
<Select
21062106
open
21072107
options={options}
2108-
optionRender={(option, {index}) => {
2108+
optionRender={(option, { index }) => {
21092109
return `${option.label} - ${index}`;
21102110
}}
21112111
/>,
@@ -2114,4 +2114,21 @@ describe('Select.Basic', () => {
21142114
'test1 - 0',
21152115
);
21162116
});
2117+
2118+
it('multiple items should not disabled', () => {
2119+
const { container } = testingRender(
2120+
<Select
2121+
open
2122+
maxCount={1}
2123+
mode="multiple"
2124+
value={['bamboo']}
2125+
options={[{ value: 'bamboo' }, { value: 'light' }]}
2126+
/>,
2127+
);
2128+
const element = container.querySelectorAll<HTMLDivElement>(
2129+
'div.rc-virtual-list-holder-inner .rc-select-item',
2130+
);
2131+
expect(element[0]).not.toHaveClass('rc-select-item-option-disabled');
2132+
expect(element[1]).toHaveClass('rc-select-item-option-disabled');
2133+
});
21172134
});

0 commit comments

Comments
 (0)