diff --git a/README.md b/README.md
index 2ea7eedc..38a8010b 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,6 @@ React TreeSelect Component
-
## Development
```
@@ -55,76 +54,77 @@ online example: https://tree-select-react-component.vercel.app/
### TreeSelect props
-| name | description | type | default |
-|----------|----------------|----------|--------------|
-|className | additional css class of root dom node | String | '' |
-|prefixCls | prefix class | String | '' |
-|animation | dropdown animation name. only support slide-up now | String | '' |
-|transitionName | dropdown css animation name | String | '' |
-|choiceTransitionName | css animation name for selected items at multiple mode | String | '' |
-|dropdownMatchSelectWidth | whether dropdown's with is same with select. Default set `min-width` same as input | bool | true |
-|dropdownClassName | additional className applied to dropdown | String | - |
-|dropdownStyle | additional style applied to dropdown | Object | {} |
-|onDropdownVisibleChange | control dropdown visible | function | `() => { return true; }` |
-|notFoundContent | specify content to show when no result matches. | String | 'Not Found' |
-|showSearch | whether show search input in single mode | bool | true |
-|allowClear | whether allowClear | bool | false |
-|maxTagTextLength | max tag text length to show | number | - |
-|maxTagCount | max tag count to show | number | - |
-|maxTagPlaceholder | placeholder for omitted values | ReactNode/function(omittedValues) | - |
-|multiple | whether multiple select (true when enable treeCheckable) | bool | false |
-|disabled | whether disabled select | bool | false |
-|searchValue | work with `onSearch` to make search value controlled. | string | '' |
-|defaultValue | initial selected treeNode(s) | same as value type | - |
-|value | current selected treeNode(s). | normal: String/Array. labelInValue: {value:String,label:React.Node}/Array<{value,label}>. treeCheckStrictly(halfChecked default false): {value:String,label:React.Node, halfChecked}/Array<{value,label,halfChecked}>. | - |
-|labelInValue| whether to embed label in value, see above value type | Bool | false |
-|onChange | called when select treeNode or input value change | function(value, label(null), extra) | - |
-|onSelect | called when select treeNode | function(value, node, extra) | - |
-|onSearch | called when input changed | function | - |
-|onTreeExpand | called when tree node expand | function(expandedKeys) | - |
-|onPopupScroll | called when popup scroll | function(event) | - |
-|showCheckedStrategy | `TreeSelect.SHOW_ALL`: show all checked treeNodes (Include parent treeNode). `TreeSelect.SHOW_PARENT`: show checked treeNodes (Just show parent treeNode). Default just show child. | enum{TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD |
-|treeIcon | show tree icon | bool | false |
-|treeLine | show tree line | bool | false |
-|treeDefaultExpandAll | default expand all treeNode | bool | false |
-|treeDefaultExpandedKeys | default expanded treeNode keys | Array | - |
-|treeExpandedKeys | set tree expanded keys | Array | - |
-|treeExpandAction | Tree open logic, optional: false \| `click` \| `doubleClick`, same as `expandAction` of `rc-tree` | string \| boolean | `click` |
-|treeCheckable | whether tree show checkbox (select callback will not fire) | bool | false |
-|treeCheckStrictly | check node precisely, parent and children nodes are not associated| bool | false |
-|filterTreeNode | whether filter treeNodes by input value. default filter by treeNode's treeNodeFilterProp prop's value | bool/Function(inputValue:string, treeNode:TreeNode) | Function |
-|treeNodeFilterProp | which prop value of treeNode will be used for filter if filterTreeNode return true | String | 'value' |
-|treeNodeLabelProp | which prop value of treeNode will render as content of select | String | 'title' |
-|treeData | treeNodes data Array, if set it then you need not to construct children TreeNode. (value should be unique across the whole array) | array<{value,label,children, [disabled,selectable]}> | [] |
-|treeDataSimpleMode | enable simple mode of treeData.(treeData should be like this: [{id:1, pId:0, value:'1', label:"test1",...},...], `pId` is parent node's id) | bool/object{id:'id', pId:'pId', rootPId:null} | false |
-|treeTitleRender | Custom render nodes | (nodeData: OptionType) => ReactNode |
-|loadData | load data asynchronously | function(node) | - |
-|getPopupContainer | container which popup select menu rendered into | function(trigger:Node):Node | function(){return document.body;} |
-|autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true |
-| suffixIcon | specify the select arrow icon | ReactNode \| (props: TreeProps) => ReactNode | - |
-| clearIcon | specify the clear icon | ReactNode \| (props: TreeProps) => ReactNode | - |
-| removeIcon | specify the remove icon | ReactNode \| (props: TreeProps) => ReactNode | - |
-|switcherIcon| specify the switcher icon | ReactNode \| (props: TreeProps) => ReactNode | - |
-|virtual| Disable virtual when `false` | false | - |
-
+| name | description | type | default |
+| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- |
+| className | additional css class of root dom node | String | '' |
+| prefixCls | prefix class | String | '' |
+| animation | dropdown animation name. only support slide-up now | String | '' |
+| transitionName | dropdown css animation name | String | '' |
+| choiceTransitionName | css animation name for selected items at multiple mode | String | '' |
+| dropdownMatchSelectWidth | whether dropdown's with is same with select. Default set `min-width` same as input | bool | true |
+| dropdownClassName | additional className applied to dropdown | String | - |
+| dropdownStyle | additional style applied to dropdown | Object | {} |
+| onDropdownVisibleChange | control dropdown visible | function | `() => { return true; }` |
+| notFoundContent | specify content to show when no result matches. | String | 'Not Found' |
+| showSearch | whether show search input in single mode | bool | true |
+| allowClear | whether allowClear | bool | false |
+| maxTagTextLength | max tag text length to show | number | - |
+| maxTagCount | max tag count to show | number | - |
+| maxCount | Limit the maximum number of items that can be selected in multiple mode | number | - |
+| maxTagPlaceholder | placeholder for omitted values | ReactNode/function(omittedValues) | - |
+| multiple | whether multiple select (true when enable treeCheckable) | bool | false |
+| disabled | whether disabled select | bool | false |
+| searchValue | work with `onSearch` to make search value controlled. | string | '' |
+| defaultValue | initial selected treeNode(s) | same as value type | - |
+| value | current selected treeNode(s). | normal: String/Array. labelInValue: {value:String,label:React.Node}/Array<{value,label}>. treeCheckStrictly(halfChecked default false): {value:String,label:React.Node, halfChecked}/Array<{value,label,halfChecked}>. | - |
+| labelInValue | whether to embed label in value, see above value type | Bool | false |
+| onChange | called when select treeNode or input value change | function(value, label(null), extra) | - |
+| onSelect | called when select treeNode | function(value, node, extra) | - |
+| onSearch | called when input changed | function | - |
+| onTreeExpand | called when tree node expand | function(expandedKeys) | - |
+| onPopupScroll | called when popup scroll | function(event) | - |
+| showCheckedStrategy | `TreeSelect.SHOW_ALL`: show all checked treeNodes (Include parent treeNode). `TreeSelect.SHOW_PARENT`: show checked treeNodes (Just show parent treeNode). Default just show child. | enum{TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD |
+| treeIcon | show tree icon | bool | false |
+| treeLine | show tree line | bool | false |
+| treeDefaultExpandAll | default expand all treeNode | bool | false |
+| treeDefaultExpandedKeys | default expanded treeNode keys | Array | - |
+| treeExpandedKeys | set tree expanded keys | Array | - |
+| treeExpandAction | Tree open logic, optional: false \| `click` \| `doubleClick`, same as `expandAction` of `rc-tree` | string \| boolean | `click` |
+| treeCheckable | whether tree show checkbox (select callback will not fire) | bool | false |
+| treeCheckStrictly | check node precisely, parent and children nodes are not associated | bool | false |
+| filterTreeNode | whether filter treeNodes by input value. default filter by treeNode's treeNodeFilterProp prop's value | bool/Function(inputValue:string, treeNode:TreeNode) | Function |
+| treeNodeFilterProp | which prop value of treeNode will be used for filter if filterTreeNode return true | String | 'value' |
+| treeNodeLabelProp | which prop value of treeNode will render as content of select | String | 'title' |
+| treeData | treeNodes data Array, if set it then you need not to construct children TreeNode. (value should be unique across the whole array) | array<{value,label,children, [disabled,selectable]}> | [] |
+| treeDataSimpleMode | enable simple mode of treeData.(treeData should be like this: [{id:1, pId:0, value:'1', label:"test1",...},...], `pId` is parent node's id) | bool/object{id:'id', pId:'pId', rootPId:null} | false |
+| treeTitleRender | Custom render nodes | (nodeData: OptionType) => ReactNode |
+| loadData | load data asynchronously | function(node) | - |
+| getPopupContainer | container which popup select menu rendered into | function(trigger:Node):Node | function(){return document.body;} |
+| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true |
+| suffixIcon | specify the select arrow icon | ReactNode \| (props: TreeProps) => ReactNode | - |
+| clearIcon | specify the clear icon | ReactNode \| (props: TreeProps) => ReactNode | - |
+| removeIcon | specify the remove icon | ReactNode \| (props: TreeProps) => ReactNode | - |
+| switcherIcon | specify the switcher icon | ReactNode \| (props: TreeProps) => ReactNode | - |
+| virtual | Disable virtual when `false` | false | - |
### TreeNode props
-> note: you'd better to use `treeData` instead of using TreeNode.
-| name | description | type | default |
-|----------|----------------|----------|--------------|
-|disabled | disable treeNode | bool | false |
-|key | it's value must be unique across the tree's all TreeNode, you must set it | String | - |
-|value | default as treeNodeFilterProp (be unique across the tree's all TreeNode) | String | '' |
-|title | tree/subTree's title | String/element | '---' |
-|isLeaf | whether it's leaf node | bool | false |
+> note: you'd better to use `treeData` instead of using TreeNode.
+| name | description | type | default |
+| -------- | ------------------------------------------------------------------------- | -------------- | ------- |
+| disabled | disable treeNode | bool | false |
+| key | it's value must be unique across the tree's all TreeNode, you must set it | String | - |
+| value | default as treeNodeFilterProp (be unique across the tree's all TreeNode) | String | '' |
+| title | tree/subTree's title | String/element | '---' |
+| isLeaf | whether it's leaf node | bool | false |
## note
+
1. Optimization tips(when there are large amounts of data, like more than 5000 nodes)
- - Do not Expand all nodes.
- - Recommend not exist many `TreeSelect` components in a page at the same time.
- - Recommend not use `treeCheckable` mode, or use `treeCheckStrictly`.
+ - Do not Expand all nodes.
+ - Recommend not exist many `TreeSelect` components in a page at the same time.
+ - Recommend not use `treeCheckable` mode, or use `treeCheckStrictly`.
2. In `treeCheckable` mode, It has the same effect when click `x`(node in Selection box) or uncheck in the treeNode(in dropdown panel), but the essence is not the same. So, even if both of them trigger `onChange` method, but the parameters (the third parameter) are different. (中文:在`treeCheckable`模式下,已选择节点上的`x`删除操作、和相应 treeNode 节点上 checkbox 的 uncheck 操作,最终效果相同,但本质不一样。前者跟弹出的 tree 组件可以“毫无关系”(例如 dropdown 没展开过,tree 也就没渲染好),而后者是 tree 组件上的节点 uncheck 事件。所以、即便两者都会触发`onChange`方法、但它们的参数(第三个参数)是不同的。)
## Test Case
@@ -137,4 +137,4 @@ http://localhost:8000/node_modules/rc-server/node_modules/node-jscover/lib/front
## License
-rc-tree-select is released under the MIT license.
\ No newline at end of file
+rc-tree-select is released under the MIT license.
diff --git a/docs/demo/mutiple-with-maxCount.md b/docs/demo/mutiple-with-maxCount.md
new file mode 100644
index 00000000..2df96692
--- /dev/null
+++ b/docs/demo/mutiple-with-maxCount.md
@@ -0,0 +1,8 @@
+---
+title: mutiple-with-maxCount
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/examples/mutiple-with-maxCount.tsx b/examples/mutiple-with-maxCount.tsx
new file mode 100644
index 00000000..41a9b77e
--- /dev/null
+++ b/examples/mutiple-with-maxCount.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import TreeSelect from '../src';
+
+export default () => {
+ const [value, setValue] = useState(['1']);
+ const [checkValue, setCheckValue] = useState(['1']);
+
+ const treeData = [
+ {
+ key: '1',
+ value: '1',
+ title: '1',
+ children: [
+ {
+ key: '1-1',
+ value: '1-1',
+ title: '1-1',
+ },
+ {
+ key: '1-2',
+ value: '1-2',
+ title: '1-2',
+ },
+ {
+ key: '1-3',
+ value: '1-3',
+ title: '1-3',
+ },
+ ],
+ },
+ {
+ key: '2',
+ value: '2',
+ title: '2',
+ },
+ {
+ key: '3',
+ value: '3',
+ title: '3',
+ },
+ {
+ key: '4',
+ value: '4',
+ title: '4',
+ },
+ ];
+
+ const onChange = (val: string[]) => {
+ setValue(val);
+ };
+
+ const onCheckChange = (val: string[]) => {
+ setCheckValue(val);
+ };
+
+ return (
+ <>
+ multiple with maxCount
+
+
+ checkable with maxCount
+
+
+ checkable with maxCount and treeCheckStrictly
+
+ >
+ );
+};
diff --git a/package.json b/package.json
index 89bf0997..ecc3e84a 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"@babel/runtime": "^7.25.7",
"classnames": "2.x",
"rc-select": "~14.16.2",
- "rc-tree": "~5.10.1",
+ "rc-tree": "~5.11.0",
"rc-util": "^5.43.0"
},
"devDependencies": {
diff --git a/src/OptionList.tsx b/src/OptionList.tsx
index bbfa37f9..b49e552e 100644
--- a/src/OptionList.tsx
+++ b/src/OptionList.tsx
@@ -2,14 +2,16 @@ import { useBaseProps } from 'rc-select';
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
import type { TreeProps } from 'rc-tree';
import Tree from 'rc-tree';
+import { UnstableContext } from 'rc-tree';
import type { EventDataNode, ScrollTo } from 'rc-tree/lib/interface';
import KeyCode from 'rc-util/lib/KeyCode';
import useMemo from 'rc-util/lib/hooks/useMemo';
import * as React from 'react';
import LegacyContext from './LegacyContext';
import TreeSelectContext from './TreeSelectContext';
-import type { Key, SafeKey } from './interface';
+import type { DataNode, Key, SafeKey } from './interface';
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
+import { useEvent } from 'rc-util';
const HIDDEN_STYLE = {
width: 0,
@@ -45,6 +47,8 @@ const OptionList: React.ForwardRefRenderFunction = (_,
treeExpandAction,
treeTitleRender,
onPopupScroll,
+ displayValues,
+ isOverMaxCount,
} = React.useContext(TreeSelectContext);
const {
@@ -76,6 +80,11 @@ const OptionList: React.ForwardRefRenderFunction = (_,
(prev, next) => next[0] && prev[1] !== next[1],
);
+ const memoRawValues = React.useMemo(
+ () => (displayValues || []).map(v => v.value),
+ [displayValues],
+ );
+
// ========================== Values ==========================
const mergedCheckedKeys = React.useMemo(() => {
if (!checkable) {
@@ -154,6 +163,10 @@ const OptionList: React.ForwardRefRenderFunction = (_,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
+ const nodeDisabled = useEvent((node: DataNode) => {
+ return isOverMaxCount && !memoRawValues.includes(node[fieldNames.value]);
+ });
+
// ========================== Get First Selectable Node ==========================
const getFirstMatchingNode = (nodes: EventDataNode[]): EventDataNode | null => {
for (const node of nodes) {
@@ -221,8 +234,9 @@ const OptionList: React.ForwardRefRenderFunction = (_,
// >>> Select item
case KeyCode.ENTER: {
if (activeEntity) {
+ const isNodeDisabled = nodeDisabled(activeEntity.node);
const { selectable, value, disabled } = activeEntity?.node || {};
- if (selectable !== false && !disabled) {
+ if (selectable !== false && !disabled && !isNodeDisabled) {
onInternalSelect(null, {
node: { key: activeKey },
selected: !checkedKeys.includes(value),
@@ -276,42 +290,43 @@ const OptionList: React.ForwardRefRenderFunction = (_,
{activeEntity.node.value}
)}
-
-
+
+
+
);
};
diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx
index db63f710..c46f49a8 100644
--- a/src/TreeSelect.tsx
+++ b/src/TreeSelect.tsx
@@ -72,6 +72,7 @@ export interface TreeSelectProps>> Data
treeData?: OptionType[];
@@ -136,6 +137,7 @@ const TreeSelect = React.forwardRef((props, ref)
treeCheckable,
treeCheckStrictly,
labelInValue,
+ maxCount,
// FieldNames
fieldNames,
@@ -413,6 +415,20 @@ const TreeSelect = React.forwardRef((props, ref)
extra: { triggerValue?: SafeKey; selected?: boolean },
source: SelectSource,
) => {
+ const formattedKeyList = formatStrategyValues(
+ newRawValues,
+ mergedShowCheckedStrategy,
+ keyEntities,
+ mergedFieldNames,
+ );
+
+ // if multiple and maxCount is set, check if exceed maxCount
+ if (mergedMultiple && maxCount !== undefined) {
+ if (formattedKeyList.length > maxCount) {
+ return;
+ }
+ }
+
const labeledValues = convert2LabelValues(newRawValues);
setInternalValue(labeledValues);
@@ -425,12 +441,6 @@ const TreeSelect = React.forwardRef((props, ref)
if (onChange) {
let eventValues: SafeKey[] = newRawValues;
if (treeConduction) {
- const formattedKeyList = formatStrategyValues(
- newRawValues,
- mergedShowCheckedStrategy,
- keyEntities,
- mergedFieldNames,
- );
eventValues = formattedKeyList.map(key => {
const entity = valueEntities.get(key);
return entity ? entity.node[mergedFieldNames.value] : key;
@@ -558,6 +568,7 @@ const TreeSelect = React.forwardRef((props, ref)
onDeselect,
rawCheckedValues,
rawHalfCheckedValues,
+ maxCount,
],
);
@@ -596,8 +607,11 @@ const TreeSelect = React.forwardRef((props, ref)
});
// ========================== Context ===========================
- const treeSelectContext = React.useMemo(
- () => ({
+ const isOverMaxCount =
+ mergedMultiple && maxCount !== undefined && cachedDisplayValues?.length >= maxCount;
+
+ const treeSelectContext = React.useMemo(() => {
+ return {
virtual,
dropdownMatchSelectWidth,
listHeight,
@@ -609,21 +623,25 @@ const TreeSelect = React.forwardRef((props, ref)
treeExpandAction,
treeTitleRender,
onPopupScroll,
- }),
- [
- virtual,
- dropdownMatchSelectWidth,
- listHeight,
- listItemHeight,
- listItemScrollOffset,
- filteredTreeData,
- mergedFieldNames,
- onOptionSelect,
- treeExpandAction,
- treeTitleRender,
- onPopupScroll,
- ],
- );
+ displayValues: cachedDisplayValues,
+ isOverMaxCount,
+ };
+ }, [
+ virtual,
+ dropdownMatchSelectWidth,
+ listHeight,
+ listItemHeight,
+ listItemScrollOffset,
+ filteredTreeData,
+ mergedFieldNames,
+ onOptionSelect,
+ treeExpandAction,
+ treeTitleRender,
+ onPopupScroll,
+ maxCount,
+ cachedDisplayValues,
+ mergedMultiple,
+ ]);
// ======================= Legacy Context =======================
const legacyContext = React.useMemo(
diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts
index b0aff525..2d335e4b 100644
--- a/src/TreeSelectContext.ts
+++ b/src/TreeSelectContext.ts
@@ -1,6 +1,6 @@
import * as React from 'react';
import type { ExpandAction } from 'rc-tree/lib/Tree';
-import type { DataNode, FieldNames, Key } from './interface';
+import type { DataNode, FieldNames, Key, LabeledValueType } from './interface';
export interface TreeSelectContextProps {
virtual?: boolean;
@@ -14,6 +14,8 @@ export interface TreeSelectContextProps {
treeExpandAction?: ExpandAction;
treeTitleRender?: (node: any) => React.ReactNode;
onPopupScroll?: React.UIEventHandler;
+ displayValues?: LabeledValueType[];
+ isOverMaxCount?: boolean;
}
const TreeSelectContext = React.createContext(null as any);
diff --git a/tests/Select.SearchInput.spec.js b/tests/Select.SearchInput.spec.js
index 77fced28..cd2d15c3 100644
--- a/tests/Select.SearchInput.spec.js
+++ b/tests/Select.SearchInput.spec.js
@@ -221,14 +221,22 @@ describe('TreeSelect.SearchInput', () => {
const input = getByRole('combobox');
fireEvent.change(input, { target: { value: '1' } });
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
+ fireEvent.keyUp(input, { keyCode: KeyCode.ENTER });
expect(onSelect).toHaveBeenCalledWith('1', expect.anything());
onSelect.mockReset();
// Search disabled node and press enter, should not select
fireEvent.change(input, { target: { value: '2' } });
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
+ fireEvent.keyUp(input, { keyCode: KeyCode.ENTER });
expect(onSelect).not.toHaveBeenCalled();
onSelect.mockReset();
+
+ // Search and press enter, should select first matched non-disabled node
+ fireEvent.change(input, { target: { value: '3' } });
+ fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
+ fireEvent.keyUp(input, { keyCode: KeyCode.ENTER });
+ expect(onSelect).toHaveBeenCalledWith('3', expect.anything());
});
it('should not select node when no matches found', () => {
diff --git a/tests/Select.maxCount.spec.tsx b/tests/Select.maxCount.spec.tsx
new file mode 100644
index 00000000..79fe48b8
--- /dev/null
+++ b/tests/Select.maxCount.spec.tsx
@@ -0,0 +1,374 @@
+import { render, fireEvent, within } from '@testing-library/react';
+import KeyCode from 'rc-util/lib/KeyCode';
+import { keyDown, keyUp } from './util';
+import React from 'react';
+import TreeSelect from '../src';
+
+describe('TreeSelect.maxCount', () => {
+ const treeData = [
+ { key: '0', value: '0', title: '0 label' },
+ { key: '1', value: '1', title: '1 label' },
+ { key: '2', value: '2', title: '2 label' },
+ { key: '3', value: '3', title: '3 label' },
+ ];
+
+ const renderTreeSelect = (props?: any) => {
+ return render();
+ };
+
+ const selectOptions = (container, optionTexts) => {
+ const dropdownList = container.querySelector('.rc-tree-select-dropdown');
+ optionTexts.forEach(text => {
+ fireEvent.click(within(dropdownList).getByText(text));
+ });
+ };
+
+ it('should disable unselected options when selection reaches maxCount', () => {
+ const { container } = renderTreeSelect();
+
+ selectOptions(container, ['0 label', '1 label']);
+
+ // Check if third and fourth options are disabled
+ const dropdownList = container.querySelector('.rc-tree-select-dropdown') as HTMLElement;
+ const option3 = within(dropdownList).getByText('2 label');
+ const option4 = within(dropdownList).getByText('3 label');
+
+ expect(option3.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
+ expect(option4.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
+ });
+
+ it('should allow deselecting options after reaching maxCount', () => {
+ const { container } = renderTreeSelect();
+ const dropdownList = container.querySelector('.rc-tree-select-dropdown') as HTMLElement;
+
+ selectOptions(container, ['0 label', '1 label']);
+
+ // Try selecting third option, should be disabled
+ const option3 = within(dropdownList).getByText('2 label');
+ fireEvent.click(option3);
+ expect(option3.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
+
+ // Deselect first option
+ fireEvent.click(within(dropdownList).getByText('0 label'));
+ expect(within(dropdownList).queryByText('0 label')).toBeInTheDocument();
+
+ // Now should be able to select third option
+ fireEvent.click(option3);
+ expect(option3.closest('div')).not.toHaveClass('rc-tree-select-tree-treenode-disabled');
+ });
+
+ it('should not trigger onChange when trying to select beyond maxCount', () => {
+ const handleChange = jest.fn();
+ const { container } = renderTreeSelect({ onChange: handleChange });
+
+ selectOptions(container, ['0 label', '1 label']);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Try selecting third option
+ const dropdownList = container.querySelector('.rc-tree-select-dropdown') as HTMLElement;
+ fireEvent.click(within(dropdownList).getByText('2 label'));
+ expect(handleChange).toHaveBeenCalledTimes(2); // Should not increase
+ });
+
+ it('should not affect deselection operations when maxCount is reached', () => {
+ const handleChange = jest.fn();
+ const { container } = renderTreeSelect({ onChange: handleChange });
+
+ selectOptions(container, ['0 label', '1 label']);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Deselect first option
+ const dropdownList = container.querySelector('.rc-tree-select-dropdown') as HTMLElement;
+ fireEvent.click(within(dropdownList).getByText('0 label'));
+ expect(handleChange).toHaveBeenCalledTimes(3);
+
+ // Should be able to select third option
+ fireEvent.click(within(dropdownList).getByText('2 label'));
+ expect(handleChange).toHaveBeenCalledTimes(4);
+ });
+
+ it('should not allow any selection when maxCount is 0', () => {
+ const handleChange = jest.fn();
+ const { container } = renderTreeSelect({ maxCount: 0, onChange: handleChange });
+
+ selectOptions(container, ['0 label', '1 label']);
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ it('should not limit selection when maxCount is greater than number of options', () => {
+ const handleChange = jest.fn();
+ const { container } = renderTreeSelect({ maxCount: 5, onChange: handleChange });
+
+ selectOptions(container, ['0 label', '1 label', '2 label', '3 label']);
+ expect(handleChange).toHaveBeenCalledTimes(4);
+ });
+
+ it('should respect maxCount when checking parent node in treeCheckable mode', () => {
+ const treeData = [
+ {
+ key: '0',
+ value: '0',
+ title: 'parent',
+ children: [
+ { key: '0-0', value: '0-0', title: 'child 1' },
+ { key: '0-1', value: '0-1', title: 'child 2' },
+ { key: '0-2', value: '0-2', title: 'child 3' },
+ ],
+ },
+ ];
+
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ // Try to check parent node which would select all children
+ const checkbox = container.querySelector('.rc-tree-select-tree-checkbox');
+ fireEvent.click(checkbox);
+
+ // onChange should not be called since it would exceed maxCount
+ expect(handleChange).not.toHaveBeenCalled();
+
+ // Parent node should still be unchecked
+ expect(checkbox).not.toHaveClass('rc-tree-select-tree-checkbox-checked');
+ });
+});
+
+describe('TreeSelect.maxCount keyboard operations', () => {
+ const treeData = [
+ { key: '0', value: '0', title: '0 label' },
+ { key: '1', value: '1', title: '1 label' },
+ { key: '2', value: '2', title: '2 label' },
+ ];
+
+ it('keyboard operations should not exceed maxCount limit', () => {
+ const onSelect = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const input = container.querySelector('input');
+
+ keyDown(input, KeyCode.ENTER);
+ keyUp(input, KeyCode.ENTER);
+
+ expect(onSelect).toHaveBeenCalledWith('0', expect.anything());
+
+ keyDown(input, KeyCode.DOWN);
+ keyDown(input, KeyCode.ENTER);
+ keyUp(input, KeyCode.ENTER);
+
+ expect(onSelect).toHaveBeenCalledWith('1', expect.anything());
+
+ keyDown(input, KeyCode.DOWN);
+ keyDown(input, KeyCode.ENTER);
+ keyUp(input, KeyCode.ENTER);
+ });
+
+ it('when maxCount is reached, the option should be disabled', () => {
+ const { container } = render(
+ ,
+ );
+
+ // verify that the third option is disabled
+ expect(container.querySelector('.rc-tree-select-tree-treenode-disabled')?.textContent).toBe(
+ '2 label',
+ );
+ });
+
+ it('should be able to unselect after reaching maxCount', () => {
+ const { container } = render(
+ ,
+ );
+
+ const input = container.querySelector('input');
+
+ // cancel first selection
+ keyDown(input, KeyCode.ENTER);
+ keyUp(input, KeyCode.ENTER);
+
+ // verify only two options are selected
+ expect(container.querySelectorAll('.rc-tree-select-tree-treenode-selected')).toHaveLength(2);
+ });
+});
+
+describe('TreeSelect.maxCount with different strategies', () => {
+ const treeData = [
+ {
+ key: '0',
+ value: '0',
+ title: 'parent',
+ children: [
+ { key: '0-0', value: '0-0', title: 'child 1' },
+ { key: '0-1', value: '0-1', title: 'child 2' },
+ { key: '0-2', value: '0-2', title: 'child 3' },
+ ],
+ },
+ ];
+
+ it('should respect maxCount with SHOW_PARENT strategy', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ // Select parent node - should work as it only shows as one option
+ const parentCheckbox = within(container).getByText('parent');
+ fireEvent.click(parentCheckbox);
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ });
+
+ it('should respect maxCount with SHOW_CHILD strategy', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ // Select parent node - should not work as it would show three children
+ const parentCheckbox = within(container).getByText('parent');
+ fireEvent.click(parentCheckbox);
+ expect(handleChange).not.toHaveBeenCalled();
+
+ // Select individual children - should work until maxCount
+ const childCheckboxes = within(container).getAllByText(/child/);
+ fireEvent.click(childCheckboxes[0]); // first child
+ fireEvent.click(childCheckboxes[1]); // second child
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Try to select third child - should not work
+ fireEvent.click(childCheckboxes[2]);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+
+ it('should respect maxCount with SHOW_ALL strategy', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ // Select parent node - should not work as it would show both parent and children
+ const parentCheckbox = within(container).getByText('parent');
+ fireEvent.click(parentCheckbox);
+ expect(handleChange).not.toHaveBeenCalled();
+
+ // Select individual children
+ const childCheckboxes = within(container).getAllByText(/child/);
+ fireEvent.click(childCheckboxes[0]);
+ fireEvent.click(childCheckboxes[1]);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+});
+
+describe('TreeSelect.maxCount with treeCheckStrictly', () => {
+ const treeData = [
+ {
+ key: '0',
+ value: '0',
+ title: 'parent',
+ children: [
+ { key: '0-0', value: '0-0', title: 'child 1' },
+ { key: '0-1', value: '0-1', title: 'child 2' },
+ ],
+ },
+ ];
+
+ it('should count parent and children separately when treeCheckStrictly is true', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ // Select parent and one child - should work as they are counted separately
+ const parentCheckbox = within(container).getByText('parent');
+ const checkboxes = within(container).getAllByText(/child/);
+ fireEvent.click(parentCheckbox);
+ fireEvent.click(checkboxes[0]); // first child
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Try to select second child - should not work as maxCount is reached
+ fireEvent.click(checkboxes[1]);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+ });
+
+ it('should allow deselecting when maxCount is reached', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const parentCheckbox = within(container).getByText('parent');
+ const checkboxes = within(container).getAllByText(/child/);
+
+ // Select parent and first child
+ fireEvent.click(parentCheckbox);
+ fireEvent.click(checkboxes[0]);
+ expect(handleChange).toHaveBeenCalledTimes(2);
+
+ // Deselect parent
+ fireEvent.click(parentCheckbox);
+ expect(handleChange).toHaveBeenCalledTimes(3);
+
+ // Now should be able to select second child
+ fireEvent.click(checkboxes[1]);
+ expect(handleChange).toHaveBeenCalledTimes(4);
+ });
+});
diff --git a/tests/Select.multiple.spec.js b/tests/Select.multiple.spec.js
index eba8a620..5f2cebca 100644
--- a/tests/Select.multiple.spec.js
+++ b/tests/Select.multiple.spec.js
@@ -1,5 +1,5 @@
/* eslint-disable no-undef */
-import { render } from '@testing-library/react';
+import { render, fireEvent, within } from '@testing-library/react';
import { mount } from 'enzyme';
import KeyCode from 'rc-util/lib/KeyCode';
import React from 'react';
@@ -32,7 +32,10 @@ describe('TreeSelect.multiple', () => {
it('remove by backspace key', () => {
const wrapper = mount(createSelect({ defaultValue: ['0', '1'] }));
- wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
+ wrapper
+ .find('input')
+ .first()
+ .simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
expect(wrapper.getSelection()).toHaveLength(1);
expect(wrapper.getSelection(0).text()).toBe('label0');
});
@@ -59,9 +62,15 @@ describe('TreeSelect.multiple', () => {
}
}
const wrapper = mount();
- wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
+ wrapper
+ .find('input')
+ .first()
+ .simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
wrapper.selectNode(1);
- wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
+ wrapper
+ .find('input')
+ .first()
+ .simulate('keyDown', { which: KeyCode.BACKSPACE, key: 'Backspace' });
expect(wrapper.getSelection()).toHaveLength(1);
expect(wrapper.getSelection(0).text()).toBe('label0');
});
@@ -337,9 +346,7 @@ describe('TreeSelect.multiple', () => {
/>,
);
- const values = Array.from(
- container.querySelectorAll('.rc-tree-select-selection-item-content'),
- ); //.map(ele => ele.textContent);
+ const values = Array.from(container.querySelectorAll('.rc-tree-select-selection-item-content')); //.map(ele => ele.textContent);
expect(values).toHaveLength(0);
@@ -347,5 +354,4 @@ describe('TreeSelect.multiple', () => {
expect(placeholder).toBeTruthy();
expect(placeholder.textContent).toBe('Fake placeholder');
});
-
});
diff --git a/tests/__snapshots__/Select.checkable.spec.tsx.snap b/tests/__snapshots__/Select.checkable.spec.tsx.snap
index d13b495b..7ee92e82 100644
--- a/tests/__snapshots__/Select.checkable.spec.tsx.snap
+++ b/tests/__snapshots__/Select.checkable.spec.tsx.snap
@@ -142,7 +142,6 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 1