Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Select support maxCount #1012

Merged
merged 12 commits into from
Dec 28, 2023
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export default () => (
| virtual | Disable virtual scroll | boolean | true |
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
| maxCount | The max number of items can be selected | number | - |

### Methods

Expand Down
8 changes: 8 additions & 0 deletions docs/demo/multiple-with-maxCount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: multiple-with-maxCount
nav:
title: Demo
path: /demo
---

<code src="../examples/multiple-with-maxCount.tsx"></code>
36 changes: 36 additions & 0 deletions docs/examples/multiple-with-maxCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable no-console */
import React from 'react';
import Select from 'rc-select';
import '../../assets/index.less';

const Test: React.FC = () => {
const [value, setValue] = React.useState<string[]>(['1']);

const onChange = (v: any) => {
setValue(v);
};

return (
<>
<h2>Multiple with maxCount</h2>
<Select
maxCount={4}
mode="multiple"
value={value}
animation="slide-up"
choiceTransitionName="rc-select-selection__choice-zoom"
style={{ width: 500 }}
optionFilterProp="children"
optionLabelProp="children"
placeholder="please select"
onChange={onChange}
options={Array.from({ length: 20 }, (_, i) => ({
label: <span>中文{i}</span>,
value: i.toString(),
}))}
/>
</>
);
};

export default Test;
29 changes: 19 additions & 10 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
onPopupScroll,
} = useBaseProps();
const {
maxCount,
flattenOptions,
onActiveValue,
defaultActiveFirstOption,
Expand All @@ -70,6 +71,11 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
// =========================== List ===========================
const listRef = React.useRef<ListRef>(null);

const overMaxCount = React.useMemo<boolean>(
() => multiple && typeof maxCount !== 'undefined' && rawValues.size >= maxCount,
[multiple, maxCount, rawValues.size],
);

const onListMouseDown: React.MouseEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
};
Expand All @@ -87,9 +93,9 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
for (let i = 0; i < len; i += 1) {
const current = (index + i * offset + len) % len;

const { group, data } = memoFlattenOptions[current];
const { group, data } = memoFlattenOptions[current] || {};

if (!group && !data.disabled) {
if (!group && !data?.disabled && !overMaxCount) {
return current;
}
}
Expand Down Expand Up @@ -198,7 +204,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
case KeyCode.ENTER: {
// value
const item = memoFlattenOptions[activeIndex];
if (item && !item.data.disabled) {
if (item && !item?.data?.disabled && !overMaxCount) {
onSelectValue(item.value);
} else {
onSelectValue(undefined);
Expand Down Expand Up @@ -256,8 +262,9 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r

const renderItem = (index: number) => {
const item = memoFlattenOptions[index];
if (!item) return null;

if (!item) {
return null;
}
const itemData = item.data || {};
const { value } = itemData;
const { group } = item;
Expand Down Expand Up @@ -327,11 +334,13 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
// Option
const selected = isSelected(value);

const mergedDisabled = disabled || (!selected && overMaxCount);

const optionPrefixCls = `${itemPrefixCls}-option`;
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, className, {
[`${optionPrefixCls}-grouped`]: groupOption,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
[`${optionPrefixCls}-disabled`]: disabled,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
[`${optionPrefixCls}-disabled`]: mergedDisabled,
[`${optionPrefixCls}-selected`]: selected,
});

Expand All @@ -356,13 +365,13 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
className={optionClassName}
title={optionTitle}
onMouseMove={() => {
if (activeIndex === itemIndex || disabled) {
if (activeIndex === itemIndex || mergedDisabled) {
return;
}
setActive(itemIndex);
}}
onClick={() => {
if (!disabled) {
if (!mergedDisabled) {
onSelectValue(value);
}
}}
Expand All @@ -380,7 +389,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
customizeIcon={menuItemSelectedIcon}
customizeIconProps={{
value,
disabled,
disabled: mergedDisabled,
isSelected: selected,
}}
>
Expand Down
8 changes: 7 additions & 1 deletion src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import OptGroup from './OptGroup';
import Option from './Option';
import OptionList from './OptionList';
import SelectContext from './SelectContext';
import type { SelectContextProps } from './SelectContext';
import useCache from './hooks/useCache';
import useFilterOptions from './hooks/useFilterOptions';
import useId from './hooks/useId';
Expand Down Expand Up @@ -156,6 +157,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
labelInValue?: boolean;
value?: ValueType | null;
defaultValue?: ValueType | null;
maxCount?: number;
onChange?: (value: ValueType, option: OptionType | OptionType[]) => void;
}

Expand Down Expand Up @@ -203,6 +205,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
defaultValue,
labelInValue,
onChange,
maxCount,

...restProps
} = props;
Expand Down Expand Up @@ -596,7 +599,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
};

// ========================== Context ===========================
const selectContext = React.useMemo(() => {
const selectContext = React.useMemo<SelectContextProps>(() => {
const realVirtual = virtual !== false && dropdownMatchSelectWidth !== false;
return {
...parsedOptions,
Expand All @@ -612,9 +615,11 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
listHeight,
listItemHeight,
childrenAsData,
maxCount,
optionRender,
};
}, [
maxCount,
parsedOptions,
displayOptions,
onActiveValue,
Expand All @@ -625,6 +630,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
mergedFieldNames,
virtual,
dropdownMatchSelectWidth,
direction,
listHeight,
listItemHeight,
childrenAsData,
Expand Down
1 change: 1 addition & 0 deletions src/SelectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface SelectContextProps {
listHeight?: number;
listItemHeight?: number;
childrenAsData?: boolean;
maxCount?: number;
}

const SelectContext = React.createContext<SelectContextProps>(null);
Expand Down
28 changes: 9 additions & 19 deletions tests/Multiple.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -640,15 +640,10 @@ describe('Select.Multiple', () => {
});
});

describe("autoClearSearchValue", () => {
describe('autoClearSearchValue', () => {
it('search value should not show when autoClearSearchValue is undefined', () => {
const wrapper = mount(
<Select
mode="multiple"
open={false}
showSearch={true}
searchValue="test"
/>,
<Select mode="multiple" open={false} showSearch={true} searchValue="test" />,
);
expect(wrapper.find('input').props().value).toBe('');
});
Expand All @@ -666,12 +661,12 @@ describe('Select.Multiple', () => {
});
it('search value should no clear when autoClearSearchValue is false', () => {
const wrapper = mount(
<Select
mode="multiple"
autoClearSearchValue={false}
showSearch={true}
searchValue="test"
/>,
<Select
mode="multiple"
autoClearSearchValue={false}
showSearch={true}
searchValue="test"
/>,
);

toggleOpen(wrapper);
Expand All @@ -680,12 +675,7 @@ describe('Select.Multiple', () => {
});
it('search value should clear when autoClearSearchValue is true', () => {
const wrapper = mount(
<Select
mode="multiple"
autoClearSearchValue={true}
showSearch={true}
searchValue="test"
/>,
<Select mode="multiple" autoClearSearchValue={true} showSearch={true} searchValue="test" />,
);
toggleOpen(wrapper);
toggleOpen(wrapper);
Expand Down
31 changes: 24 additions & 7 deletions tests/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ describe('Select.Basic', () => {
});

describe('click input will trigger focus', () => {
let handleFocus;
let handleFocus: jest.Mock;
let wrapper;
beforeEach(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -690,15 +690,15 @@ describe('Select.Basic', () => {
});

it('focus input when placeholder is clicked', () => {
const wrapper = mount(
const selectWrapper = mount(
<Select placeholder="xxxx">
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
);
const inputSpy = jest.spyOn(wrapper.find('input').instance(), 'focus' as any);
wrapper.find('.rc-select-selection-placeholder').simulate('mousedown');
wrapper.find('.rc-select-selection-placeholder').simulate('click');
const inputSpy = jest.spyOn(selectWrapper.find('input').instance(), 'focus' as any);
selectWrapper.find('.rc-select-selection-placeholder').simulate('mousedown');
selectWrapper.find('.rc-select-selection-placeholder').simulate('click');
expect(inputSpy).toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -1499,7 +1499,7 @@ describe('Select.Basic', () => {
);
expect(menuItemSelectedIcon).toHaveBeenCalledWith({
value: '1',
disabled: undefined,
disabled: false,
isSelected: true,
});

Expand Down Expand Up @@ -2105,7 +2105,7 @@ describe('Select.Basic', () => {
<Select
open
options={options}
optionRender={(option, {index}) => {
optionRender={(option, { index }) => {
return `${option.label} - ${index}`;
}}
/>,
Expand All @@ -2114,4 +2114,21 @@ describe('Select.Basic', () => {
'test1 - 0',
);
});

it('multiple items should not disabled', () => {
const { container } = testingRender(
<Select
open
maxCount={1}
mode="multiple"
value={['bamboo']}
options={[{ value: 'bamboo' }, { value: 'light' }]}
/>,
);
const element = container.querySelectorAll<HTMLDivElement>(
'div.rc-virtual-list-holder-inner .rc-select-item',
);
expect(element[0]).not.toHaveClass('rc-select-item-option-disabled');
expect(element[1]).toHaveClass('rc-select-item-option-disabled');
});
});
Loading