feat: Select support maxCount prop (#46667)

* feat: Select support maxCount prop

* fix: fix snap

* chore: fix

* fix: fix

* fix: fix

* rename: .tsx => .ts

* docs: update docs

* Update components/select/demo/maxCount.md

Co-authored-by: MadCcc <madccc@foxmail.com>
Signed-off-by: lijianan <574980606@qq.com>

* Revert "rename: .tsx => .ts"

This reverts commit c9c5f5acbe.

* fix: update rc-select

* Update components/select/demo/maxCount.md

Co-authored-by: MadCcc <madccc@foxmail.com>
Signed-off-by: lijianan <574980606@qq.com>

* Update components/select/index.tsx

Co-authored-by: MadCcc <madccc@foxmail.com>
Signed-off-by: lijianan <574980606@qq.com>

* update demo

---------

Signed-off-by: lijianan <574980606@qq.com>
Co-authored-by: MadCcc <madccc@foxmail.com>
This commit is contained in:
lijianan 2024-01-02 17:41:50 +08:00 committed by GitHub
parent bfda8cea90
commit 57521a01e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 490 additions and 5 deletions

View File

@ -5491,6 +5491,297 @@ exports[`renders components/select/demo/label-in-value.tsx extend context correc
exports[`renders components/select/demo/label-in-value.tsx extend context correctly 2`] = `[]`;
exports[`renders components/select/demo/maxCount.tsx extend context correctly 1`] = `
<div
class="ant-select ant-select-outlined ant-select-multiple ant-select-show-arrow ant-select-show-search"
style="width: 100%;"
>
<div
class="ant-select-selector"
>
<div
class="ant-select-selection-overflow"
>
<div
class="ant-select-selection-overflow-item"
style="opacity: 1;"
>
<span
class="ant-select-selection-item"
title="Ava Swift"
>
<span
class="ant-select-selection-item-content"
>
Ava Swift
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select: none;"
unselectable="on"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</span>
</div>
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity: 1;"
>
<div
class="ant-select-selection-search"
style="width: 0px;"
>
<input
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-expanded="false"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
readonly=""
role="combobox"
style="opacity: 0;"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
</span>
</div>
</div>
</div>
</div>
<div
class="ant-select-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up ant-select-dropdown-placement-bottomLeft"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div>
<div
id="rc_select_TEST_OR_SSR_list"
role="listbox"
style="height: 0px; width: 0px; overflow: hidden;"
>
<div
aria-label="Ava Swift"
aria-selected="true"
id="rc_select_TEST_OR_SSR_list_0"
role="option"
>
Ava Swift
</div>
<div
aria-label="Cole Reed"
aria-selected="false"
id="rc_select_TEST_OR_SSR_list_1"
role="option"
>
Cole Reed
</div>
</div>
<div
class="rc-virtual-list"
style="position: relative;"
>
<div
class="rc-virtual-list-holder"
style="max-height: 256px; overflow-y: auto;"
>
<div>
<div
class="rc-virtual-list-holder-inner"
style="display: flex; flex-direction: column;"
>
<div
aria-selected="true"
class="ant-select-item ant-select-item-option ant-select-item-option-active ant-select-item-option-selected"
title="Ava Swift"
>
<div
class="ant-select-item-option-content"
>
Ava Swift
</div>
<span
aria-hidden="true"
class="ant-select-item-option-state"
style="user-select: none;"
unselectable="on"
>
<span
aria-label="check"
class="anticon anticon-check"
role="img"
>
<svg
aria-hidden="true"
data-icon="check"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</span>
</span>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Cole Reed"
>
<div
class="ant-select-item-option-content"
>
Cole Reed
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Mia Blake"
>
<div
class="ant-select-item-option-content"
>
Mia Blake
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Jake Stone"
>
<div
class="ant-select-item-option-content"
>
Jake Stone
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Lily Lane"
>
<div
class="ant-select-item-option-content"
>
Lily Lane
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Ryan Chase"
>
<div
class="ant-select-item-option-content"
>
Ryan Chase
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Zoe Fox"
>
<div
class="ant-select-item-option-content"
>
Zoe Fox
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Alex Grey"
>
<div
class="ant-select-item-option-content"
>
Alex Grey
</div>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="Elle Blair"
>
<div
class="ant-select-item-option-content"
>
Elle Blair
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select: none;"
unselectable="on"
>
<span>
1 / 3
</span>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
`;
exports[`renders components/select/demo/maxCount.tsx extend context correctly 2`] = `[]`;
exports[`renders components/select/demo/multiple.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"

View File

@ -1740,6 +1740,127 @@ exports[`renders components/select/demo/label-in-value.tsx correctly 1`] = `
</div>
`;
exports[`renders components/select/demo/maxCount.tsx correctly 1`] = `
<div
class="ant-select ant-select-outlined ant-select-multiple ant-select-show-arrow ant-select-show-search"
style="width:100%"
>
<div
class="ant-select-selector"
>
<div
class="ant-select-selection-overflow"
>
<div
class="ant-select-selection-overflow-item"
style="opacity:1"
>
<span
class="ant-select-selection-item"
title="Ava Swift"
>
<span
class="ant-select-selection-item-content"
>
Ava Swift
</span>
<span
aria-hidden="true"
class="ant-select-selection-item-remove"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</span>
</span>
</div>
<div
class="ant-select-selection-overflow-item ant-select-selection-overflow-item-suffix"
style="opacity:1"
>
<div
class="ant-select-selection-search"
style="width:0"
>
<input
aria-autocomplete="list"
aria-controls="undefined_list"
aria-expanded="false"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
<span
aria-hidden="true"
class="ant-select-selection-search-mirror"
>
</span>
</div>
</div>
</div>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
>
<span>
1
<!-- -->
/
<!-- -->
3
</span>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
`;
exports[`renders components/select/demo/multiple.tsx correctly 1`] = `
<div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"

View File

@ -1,12 +1,13 @@
import { CloseOutlined } from '@ant-design/icons';
import React from 'react';
import { CloseOutlined } from '@ant-design/icons';
import type { SelectProps } from '..';
import Select from '..';
import { resetWarned } from '../../_util/warning';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, fireEvent, render } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
const { Option } = Select;
@ -185,5 +186,15 @@ describe('Select', () => {
errSpy.mockRestore();
});
it('Select maxCount warning', () => {
resetWarned();
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Select maxCount={10} />);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Select] `maxCount` only works with mode `multiple` or `tags`',
);
errSpy.mockRestore();
});
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
你可以通过设置 `maxCount` 约束最多可选中的数量,当超出限制时会变成禁止选中状态。
## en-US
You can set the `maxCount` prop to control the max number of items can be selected. When the limit is exceeded, the options will become disabled.

View File

@ -0,0 +1,43 @@
import React from 'react';
import { DownOutlined } from '@ant-design/icons';
import { Select } from 'antd';
const MAX_COUNT = 3;
const App: React.FC = () => {
const [value, setValue] = React.useState<string[]>(['Ava Swift']);
const suffix = (
<>
<span>
{value.length} / {MAX_COUNT}
</span>
<DownOutlined />
</>
);
return (
<Select
mode="multiple"
maxCount={MAX_COUNT}
value={value}
style={{ width: '100%' }}
onChange={setValue}
suffixIcon={suffix}
placeholder="Please select"
options={[
{ value: 'Ava Swift', label: 'Ava Swift' },
{ value: 'Cole Reed', label: 'Cole Reed' },
{ value: 'Mia Blake', label: 'Mia Blake' },
{ value: 'Jake Stone', label: 'Jake Stone' },
{ value: 'Lily Lane', label: 'Lily Lane' },
{ value: 'Ryan Chase', label: 'Ryan Chase' },
{ value: 'Zoe Fox', label: 'Zoe Fox' },
{ value: 'Alex Grey', label: 'Alex Grey' },
{ value: 'Elle Blair', label: 'Elle Blair' },
]}
/>
);
};
export default App;

View File

@ -48,6 +48,7 @@ Select component to select value from options.
<code src="./demo/option-label-center.tsx" debug>Options label Centered</code>
<code src="./demo/debug-flip-shift.tsx" iframe="200" debug>Flip + Shift</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
<code src="./demo/maxCount.tsx" version="5.13.0">Max Count</code>
## API
@ -75,6 +76,7 @@ Common props ref[Common props](/docs/react/common-props)
| labelInValue | Whether to embed label in value, turn the format of value from `string` to { value: string, label: ReactNode } | boolean | false | |
| listHeight | Config popup height | number | 256 | |
| loading | Indicate loading state | boolean | false | |
| maxCount | The max number of items can be selected, only applies when `mode` is `multiple` or `tags` | number | - | 5.13.0 |
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | Max tag text length to show | number | - | |

View File

@ -117,6 +117,7 @@ const InternalSelect = <
dropdownStyle,
transitionName,
tagRender,
maxCount,
...rest
} = props;
@ -263,6 +264,12 @@ const InternalSelect = <
);
warning.deprecated(!('bordered' in props), 'bordered', 'variant');
warning(
!(typeof maxCount !== 'undefined' && !isMultiple),
'usage',
'`maxCount` only works with mode `multiple` or `tags`',
);
}
// ====================== zIndex =========================
@ -282,6 +289,7 @@ const InternalSelect = <
listHeight={listHeight}
listItemHeight={listItemHeight}
mode={mode}
maxCount={isMultiple ? maxCount : undefined}
prefixCls={prefixCls}
placement={memoPlacement}
direction={direction}

View File

@ -49,6 +49,7 @@ demo:
<code src="./demo/option-label-center.tsx" debug>选项文本居中</code>
<code src="./demo/debug-flip-shift.tsx" iframe="200" debug>翻转+偏移</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
<code src="./demo/maxCount.tsx" version="5.13.0">最大选中数量</code>
## API
@ -76,6 +77,7 @@ demo:
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 { value: string, label: ReactNode } 的格式 | boolean | false | |
| listHeight | 设置弹窗滚动高度 | number | 256 | |
| loading | 加载中状态 | boolean | false | |
| maxCount | 指定可选中的最多 items 数量,仅在 `mode``multiple``tags` 时生效 | number | - | 5.13.0 |
| maxTagCount | 最多显示多少个 tag响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |

View File

@ -127,7 +127,7 @@
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.10",
"qrcode.react": "^3.1.0",
"rc-cascader": "~3.20.0",
"rc-cascader": "~3.21.0",
"rc-checkbox": "~3.1.0",
"rc-collapse": "~3.7.2",
"rc-dialog": "~9.3.4",
@ -147,7 +147,7 @@
"rc-rate": "~2.12.0",
"rc-resize-observer": "^1.4.0",
"rc-segmented": "~2.2.2",
"rc-select": "~14.10.0",
"rc-select": "~14.11.0",
"rc-slider": "~10.5.0",
"rc-steps": "~6.0.1",
"rc-switch": "~4.1.0",
@ -156,7 +156,7 @@
"rc-textarea": "~1.6.3",
"rc-tooltip": "~6.1.3",
"rc-tree": "~5.8.2",
"rc-tree-select": "~5.15.0",
"rc-tree-select": "~5.17.0",
"rc-upload": "~4.5.2",
"rc-util": "^5.38.1",
"scroll-into-view-if-needed": "^3.1.0",