Skip to content

Commit c8b2c92

Browse files
committed
feat: supplement maxCount logic for complicated cases
1 parent c96aee2 commit c8b2c92

File tree

5 files changed

+179
-48
lines changed

5 files changed

+179
-48
lines changed

examples/mutiple-with-maxCount.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ export default () => {
7777
maxCount={3}
7878
treeData={treeData}
7979
/>
80-
8180
<h2>checkable with maxCount</h2>
8281
<TreeSelect
8382
style={{ width: 300 }}
@@ -90,7 +89,6 @@ export default () => {
9089
onChange={onChange}
9190
value={value}
9291
/>
93-
9492
<h2>checkable with maxCount and treeCheckStrictly</h2>
9593
<TreeSelect
9694
style={{ width: 300 }}

src/OptionList.tsx

+48-43
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import useMemo from 'rc-util/lib/hooks/useMemo';
99
import * as React from 'react';
1010
import LegacyContext from './LegacyContext';
1111
import TreeSelectContext from './TreeSelectContext';
12-
import type { DataNode, Key, SafeKey } from './interface';
12+
import type { DataNode, FieldNames, Key, SafeKey } from './interface';
1313
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
1414
import { useEvent } from 'rc-util';
1515
import { formatStrategyValues } from './utils/strategyUtil';
@@ -49,7 +49,6 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
4949
treeExpandAction,
5050
treeTitleRender,
5151
onPopupScroll,
52-
displayValues,
5352
isOverMaxCount,
5453
maxCount,
5554
showCheckedStrategy,
@@ -84,11 +83,6 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
8483
(prev, next) => next[0] && prev[1] !== next[1],
8584
);
8685

87-
const memoRawValues = React.useMemo(
88-
() => (displayValues || []).map(v => v.value),
89-
[displayValues],
90-
);
91-
9286
// ========================== Values ==========================
9387
const mergedCheckedKeys = React.useMemo(() => {
9488
if (!checkable) {
@@ -167,58 +161,69 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
167161
// eslint-disable-next-line react-hooks/exhaustive-deps
168162
}, [searchValue]);
169163

164+
const disabledCacheRef = React.useRef(new Map<Key, boolean>());
165+
const lastCheckedKeysRef = React.useRef<Key[]>([]);
166+
const lastMaxCountRef = React.useRef<number>(null);
167+
168+
const resetCache = React.useCallback(() => {
169+
disabledCacheRef.current.clear();
170+
lastCheckedKeysRef.current = [...checkedKeys];
171+
lastMaxCountRef.current = maxCount;
172+
}, [checkedKeys, maxCount]);
173+
174+
React.useEffect(() => {
175+
resetCache();
176+
}, [checkedKeys, maxCount]);
177+
178+
const getSelectableKeys = (targetNode: DataNode, fieldNames: FieldNames): Key[] => {
179+
const keys = [targetNode[fieldNames.value]];
180+
if (!Array.isArray(targetNode.children)) {
181+
return keys;
182+
}
183+
184+
return targetNode.children.reduce((acc, child) => {
185+
if (!child.disabled) {
186+
acc.push(...getSelectableKeys(child, fieldNames));
187+
}
188+
return acc;
189+
}, keys);
190+
};
191+
170192
const nodeDisabled = useEvent((node: DataNode) => {
171-
// Always enable selected nodes
172-
if (checkedKeys.includes(node[fieldNames.value])) {
193+
const nodeValue = node[fieldNames.value];
194+
195+
if (checkedKeys.includes(nodeValue)) {
173196
return false;
174197
}
175198

176-
// Get all selectable keys under current node considering conduction rules
177-
const getSelectableKeys = (nodes: DataNode[]) => {
178-
const keys: Key[] = [];
179-
const traverse = (n: DataNode) => {
180-
if (!n.disabled) {
181-
keys.push(n[fieldNames.value]);
182-
// Only traverse children if node is not disabled
183-
if (Array.isArray(n.children)) {
184-
n.children.forEach(traverse);
185-
}
186-
}
187-
};
188-
nodes.forEach(traverse);
189-
return keys;
190-
};
199+
if (isOverMaxCount) {
200+
return true;
201+
}
191202

192-
const selectableNodeValues = getSelectableKeys([node]);
203+
const cacheKey = `${nodeValue}-${checkedKeys.join(',')}-${maxCount}`;
193204

194-
// Simulate checked state after selecting current node
195-
const simulatedCheckedKeys = [...checkedKeys, ...selectableNodeValues];
205+
// check cache
206+
if (disabledCacheRef.current.has(cacheKey)) {
207+
return disabledCacheRef.current.get(cacheKey);
208+
}
196209

210+
// calculate disabled state
211+
const selectableNodeKeys = getSelectableKeys(node, fieldNames);
212+
const simulatedCheckedKeys = [...checkedKeys, ...selectableNodeKeys];
197213
const { checkedKeys: conductedKeys } = conductCheck(simulatedCheckedKeys, true, keyEntities);
198-
199-
// Calculate display keys based on strategy
200-
const simulatedDisplayKeys = formatStrategyValues(
214+
const simulatedDisplayValues = formatStrategyValues(
201215
conductedKeys as SafeKey[],
202216
showCheckedStrategy,
203217
keyEntities,
204218
fieldNames,
205219
);
206220

207-
const currentDisplayKeys = formatStrategyValues(
208-
checkedKeys as SafeKey[],
209-
showCheckedStrategy,
210-
keyEntities,
211-
fieldNames,
212-
);
221+
const isDisabled = simulatedDisplayValues.length > maxCount;
213222

214-
const newDisplayValuesCount = simulatedDisplayKeys.length - currentDisplayKeys.length;
215-
216-
// Check if selecting this node would exceed maxCount
217-
if (isOverMaxCount || memoRawValues.length + newDisplayValuesCount > maxCount) {
218-
return true;
219-
}
223+
// update cache
224+
disabledCacheRef.current.set(cacheKey, isDisabled);
220225

221-
return false;
226+
return isDisabled;
222227
});
223228

224229
// ========================== Get First Selectable Node ==========================

src/TreeSelect.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
422422
mergedFieldNames,
423423
);
424424

425+
console.log('triggerChange');
426+
425427
const labeledValues = convert2LabelValues(newRawValues);
426428
setInternalValue(labeledValues);
427429

@@ -616,7 +618,6 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
616618
treeExpandAction,
617619
treeTitleRender,
618620
onPopupScroll,
619-
displayValues: cachedDisplayValues,
620621
isOverMaxCount,
621622
maxCount,
622623
showCheckedStrategy: mergedShowCheckedStrategy,
@@ -634,7 +635,6 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
634635
treeTitleRender,
635636
onPopupScroll,
636637
maxCount,
637-
cachedDisplayValues,
638638
mergedMultiple,
639639
mergedShowCheckedStrategy,
640640
]);

src/TreeSelectContext.ts

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export interface TreeSelectContextProps {
1515
treeExpandAction?: ExpandAction;
1616
treeTitleRender?: (node: any) => React.ReactNode;
1717
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
18-
displayValues?: LabeledValueType[];
1918
isOverMaxCount?: boolean;
2019
maxCount?: number;
2120
showCheckedStrategy?: CheckedStrategy;

tests/Select.maxCount.spec.tsx

+129
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,132 @@ describe('TreeSelect.maxCount with treeCheckStrictly', () => {
372372
expect(handleChange).toHaveBeenCalledTimes(4);
373373
});
374374
});
375+
376+
describe('TreeSelect.maxCount with complex scenarios', () => {
377+
const complexTreeData = [
378+
{
379+
key: 'asia',
380+
value: 'asia',
381+
title: 'Asia',
382+
children: [
383+
{
384+
key: 'china',
385+
value: 'china',
386+
title: 'China',
387+
children: [
388+
{ key: 'beijing', value: 'beijing', title: 'Beijing' },
389+
{ key: 'shanghai', value: 'shanghai', title: 'Shanghai' },
390+
{ key: 'guangzhou', value: 'guangzhou', title: 'Guangzhou' },
391+
],
392+
},
393+
{
394+
key: 'japan',
395+
value: 'japan',
396+
title: 'Japan',
397+
children: [
398+
{ key: 'tokyo', value: 'tokyo', title: 'Tokyo' },
399+
{ key: 'osaka', value: 'osaka', title: 'Osaka' },
400+
],
401+
},
402+
],
403+
},
404+
{
405+
key: 'europe',
406+
value: 'europe',
407+
title: 'Europe',
408+
children: [
409+
{
410+
key: 'uk',
411+
value: 'uk',
412+
title: 'United Kingdom',
413+
children: [
414+
{ key: 'london', value: 'london', title: 'London' },
415+
{ key: 'manchester', value: 'manchester', title: 'Manchester' },
416+
],
417+
},
418+
{
419+
key: 'france',
420+
value: 'france',
421+
title: 'France',
422+
disabled: true,
423+
children: [
424+
{ key: 'paris', value: 'paris', title: 'Paris' },
425+
{ key: 'lyon', value: 'lyon', title: 'Lyon' },
426+
],
427+
},
428+
],
429+
},
430+
];
431+
432+
it('should handle complex tree structure with maxCount correctly', () => {
433+
const handleChange = jest.fn();
434+
const { getByRole } = render(
435+
<TreeSelect
436+
treeData={complexTreeData}
437+
treeCheckable
438+
treeDefaultExpandAll
439+
multiple
440+
maxCount={3}
441+
onChange={handleChange}
442+
open
443+
/>,
444+
);
445+
446+
const container = getByRole('tree');
447+
448+
// 选择一个顶层节点
449+
const asiaNode = within(container).getByText('Asia');
450+
fireEvent.click(asiaNode);
451+
expect(handleChange).not.toHaveBeenCalled(); // 不应该触发,因为会超过 maxCount
452+
453+
// 选择叶子节点
454+
const beijingNode = within(container).getByText('Beijing');
455+
const shanghaiNode = within(container).getByText('Shanghai');
456+
const tokyoNode = within(container).getByText('Tokyo');
457+
const londonNode = within(container).getByText('London');
458+
459+
fireEvent.click(beijingNode);
460+
fireEvent.click(shanghaiNode);
461+
fireEvent.click(tokyoNode);
462+
expect(handleChange).toHaveBeenCalledTimes(3);
463+
464+
// 尝试选择第四个节点,应该被阻止
465+
fireEvent.click(londonNode);
466+
expect(handleChange).toHaveBeenCalledTimes(3);
467+
468+
// 验证禁用状态
469+
expect(londonNode.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
470+
});
471+
472+
it('should handle maxCount with mixed selection strategies', () => {
473+
const handleChange = jest.fn();
474+
475+
const { getByRole } = render(
476+
<TreeSelect
477+
treeData={complexTreeData}
478+
treeCheckable
479+
treeDefaultExpandAll
480+
multiple
481+
maxCount={3}
482+
onChange={handleChange}
483+
defaultValue={['uk']}
484+
open
485+
/>,
486+
);
487+
488+
const container = getByRole('tree');
489+
490+
const tokyoNode = within(container).getByText('Tokyo');
491+
fireEvent.click(tokyoNode);
492+
493+
// because UK node will show two children, so it will trigger one change
494+
expect(handleChange).toHaveBeenCalledTimes(1);
495+
496+
const beijingNode = within(container).getByText('Beijing');
497+
fireEvent.click(beijingNode);
498+
499+
// should not trigger change
500+
expect(handleChange).toHaveBeenCalledTimes(1);
501+
expect(beijingNode.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
502+
});
503+
});

0 commit comments

Comments
 (0)