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;
24 changes: 16 additions & 8 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 shouldDisabled = React.useMemo<boolean>(
li-jia-nan marked this conversation as resolved.
Show resolved Hide resolved
() => 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 && !shouldDisabled) {
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 && !shouldDisabled) {
onSelectValue(item.value);
} else {
onSelectValue(undefined);
Expand Down Expand Up @@ -324,14 +330,16 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
const { disabled, title, children, style, className, ...otherProps } = data;
const passedProps = omit(otherProps, omitFieldNameList);

const mergedDisabled = disabled || shouldDisabled;

// Option
const selected = isSelected(value);

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 +364,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 +388,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
14 changes: 7 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 Down
Loading