Skip to content

Commit

Permalink
feat: Select support maxCount (#1012)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
li-jia-nan committed Dec 28, 2023
1 parent 56fd0d9 commit d8a13f3
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 37 deletions.
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');
});
});

1 comment on commit d8a13f3

@vercel
Copy link

@vercel vercel bot commented on d8a13f3 Dec 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

select – ./

select-git-master-react-component.vercel.app
select.vercel.app
select-react-component.vercel.app

Please sign in to comment.