From c8b2c9242b3c419b73d15f0796eded03b624a072 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com>
Date: Fri, 6 Dec 2024 16:36:07 +0800
Subject: [PATCH] feat: supplement maxCount logic for complicated cases
---
examples/mutiple-with-maxCount.tsx | 2 -
src/OptionList.tsx | 91 ++++++++++----------
src/TreeSelect.tsx | 4 +-
src/TreeSelectContext.ts | 1 -
tests/Select.maxCount.spec.tsx | 129 +++++++++++++++++++++++++++++
5 files changed, 179 insertions(+), 48 deletions(-)
diff --git a/examples/mutiple-with-maxCount.tsx b/examples/mutiple-with-maxCount.tsx
index ca192a37..738861d0 100644
--- a/examples/mutiple-with-maxCount.tsx
+++ b/examples/mutiple-with-maxCount.tsx
@@ -77,7 +77,6 @@ export default () => {
maxCount={3}
treeData={treeData}
/>
-
checkable with maxCount
{
onChange={onChange}
value={value}
/>
-
checkable with maxCount and treeCheckStrictly
= (_,
treeExpandAction,
treeTitleRender,
onPopupScroll,
- displayValues,
isOverMaxCount,
maxCount,
showCheckedStrategy,
@@ -84,11 +83,6 @@ 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) {
@@ -167,58 +161,69 @@ const OptionList: React.ForwardRefRenderFunction = (_,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
+ const disabledCacheRef = React.useRef(new Map());
+ const lastCheckedKeysRef = React.useRef([]);
+ const lastMaxCountRef = React.useRef(null);
+
+ const resetCache = React.useCallback(() => {
+ disabledCacheRef.current.clear();
+ lastCheckedKeysRef.current = [...checkedKeys];
+ lastMaxCountRef.current = maxCount;
+ }, [checkedKeys, maxCount]);
+
+ React.useEffect(() => {
+ resetCache();
+ }, [checkedKeys, maxCount]);
+
+ const getSelectableKeys = (targetNode: DataNode, fieldNames: FieldNames): Key[] => {
+ const keys = [targetNode[fieldNames.value]];
+ if (!Array.isArray(targetNode.children)) {
+ return keys;
+ }
+
+ return targetNode.children.reduce((acc, child) => {
+ if (!child.disabled) {
+ acc.push(...getSelectableKeys(child, fieldNames));
+ }
+ return acc;
+ }, keys);
+ };
+
const nodeDisabled = useEvent((node: DataNode) => {
- // Always enable selected nodes
- if (checkedKeys.includes(node[fieldNames.value])) {
+ const nodeValue = node[fieldNames.value];
+
+ if (checkedKeys.includes(nodeValue)) {
return false;
}
- // Get all selectable keys under current node considering conduction rules
- const getSelectableKeys = (nodes: DataNode[]) => {
- const keys: Key[] = [];
- const traverse = (n: DataNode) => {
- if (!n.disabled) {
- keys.push(n[fieldNames.value]);
- // Only traverse children if node is not disabled
- if (Array.isArray(n.children)) {
- n.children.forEach(traverse);
- }
- }
- };
- nodes.forEach(traverse);
- return keys;
- };
+ if (isOverMaxCount) {
+ return true;
+ }
- const selectableNodeValues = getSelectableKeys([node]);
+ const cacheKey = `${nodeValue}-${checkedKeys.join(',')}-${maxCount}`;
- // Simulate checked state after selecting current node
- const simulatedCheckedKeys = [...checkedKeys, ...selectableNodeValues];
+ // check cache
+ if (disabledCacheRef.current.has(cacheKey)) {
+ return disabledCacheRef.current.get(cacheKey);
+ }
+ // calculate disabled state
+ const selectableNodeKeys = getSelectableKeys(node, fieldNames);
+ const simulatedCheckedKeys = [...checkedKeys, ...selectableNodeKeys];
const { checkedKeys: conductedKeys } = conductCheck(simulatedCheckedKeys, true, keyEntities);
-
- // Calculate display keys based on strategy
- const simulatedDisplayKeys = formatStrategyValues(
+ const simulatedDisplayValues = formatStrategyValues(
conductedKeys as SafeKey[],
showCheckedStrategy,
keyEntities,
fieldNames,
);
- const currentDisplayKeys = formatStrategyValues(
- checkedKeys as SafeKey[],
- showCheckedStrategy,
- keyEntities,
- fieldNames,
- );
+ const isDisabled = simulatedDisplayValues.length > maxCount;
- const newDisplayValuesCount = simulatedDisplayKeys.length - currentDisplayKeys.length;
-
- // Check if selecting this node would exceed maxCount
- if (isOverMaxCount || memoRawValues.length + newDisplayValuesCount > maxCount) {
- return true;
- }
+ // update cache
+ disabledCacheRef.current.set(cacheKey, isDisabled);
- return false;
+ return isDisabled;
});
// ========================== Get First Selectable Node ==========================
diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx
index aeb089bd..7eeb7729 100644
--- a/src/TreeSelect.tsx
+++ b/src/TreeSelect.tsx
@@ -422,6 +422,8 @@ const TreeSelect = React.forwardRef((props, ref)
mergedFieldNames,
);
+ console.log('triggerChange');
+
const labeledValues = convert2LabelValues(newRawValues);
setInternalValue(labeledValues);
@@ -616,7 +618,6 @@ const TreeSelect = React.forwardRef((props, ref)
treeExpandAction,
treeTitleRender,
onPopupScroll,
- displayValues: cachedDisplayValues,
isOverMaxCount,
maxCount,
showCheckedStrategy: mergedShowCheckedStrategy,
@@ -634,7 +635,6 @@ const TreeSelect = React.forwardRef((props, ref)
treeTitleRender,
onPopupScroll,
maxCount,
- cachedDisplayValues,
mergedMultiple,
mergedShowCheckedStrategy,
]);
diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts
index f3ceb1cf..c1a7f042 100644
--- a/src/TreeSelectContext.ts
+++ b/src/TreeSelectContext.ts
@@ -15,7 +15,6 @@ export interface TreeSelectContextProps {
treeExpandAction?: ExpandAction;
treeTitleRender?: (node: any) => React.ReactNode;
onPopupScroll?: React.UIEventHandler;
- displayValues?: LabeledValueType[];
isOverMaxCount?: boolean;
maxCount?: number;
showCheckedStrategy?: CheckedStrategy;
diff --git a/tests/Select.maxCount.spec.tsx b/tests/Select.maxCount.spec.tsx
index 79fe48b8..31a9a68b 100644
--- a/tests/Select.maxCount.spec.tsx
+++ b/tests/Select.maxCount.spec.tsx
@@ -372,3 +372,132 @@ describe('TreeSelect.maxCount with treeCheckStrictly', () => {
expect(handleChange).toHaveBeenCalledTimes(4);
});
});
+
+describe('TreeSelect.maxCount with complex scenarios', () => {
+ const complexTreeData = [
+ {
+ key: 'asia',
+ value: 'asia',
+ title: 'Asia',
+ children: [
+ {
+ key: 'china',
+ value: 'china',
+ title: 'China',
+ children: [
+ { key: 'beijing', value: 'beijing', title: 'Beijing' },
+ { key: 'shanghai', value: 'shanghai', title: 'Shanghai' },
+ { key: 'guangzhou', value: 'guangzhou', title: 'Guangzhou' },
+ ],
+ },
+ {
+ key: 'japan',
+ value: 'japan',
+ title: 'Japan',
+ children: [
+ { key: 'tokyo', value: 'tokyo', title: 'Tokyo' },
+ { key: 'osaka', value: 'osaka', title: 'Osaka' },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'europe',
+ value: 'europe',
+ title: 'Europe',
+ children: [
+ {
+ key: 'uk',
+ value: 'uk',
+ title: 'United Kingdom',
+ children: [
+ { key: 'london', value: 'london', title: 'London' },
+ { key: 'manchester', value: 'manchester', title: 'Manchester' },
+ ],
+ },
+ {
+ key: 'france',
+ value: 'france',
+ title: 'France',
+ disabled: true,
+ children: [
+ { key: 'paris', value: 'paris', title: 'Paris' },
+ { key: 'lyon', value: 'lyon', title: 'Lyon' },
+ ],
+ },
+ ],
+ },
+ ];
+
+ it('should handle complex tree structure with maxCount correctly', () => {
+ const handleChange = jest.fn();
+ const { getByRole } = render(
+ ,
+ );
+
+ const container = getByRole('tree');
+
+ // 选择一个顶层节点
+ const asiaNode = within(container).getByText('Asia');
+ fireEvent.click(asiaNode);
+ expect(handleChange).not.toHaveBeenCalled(); // 不应该触发,因为会超过 maxCount
+
+ // 选择叶子节点
+ const beijingNode = within(container).getByText('Beijing');
+ const shanghaiNode = within(container).getByText('Shanghai');
+ const tokyoNode = within(container).getByText('Tokyo');
+ const londonNode = within(container).getByText('London');
+
+ fireEvent.click(beijingNode);
+ fireEvent.click(shanghaiNode);
+ fireEvent.click(tokyoNode);
+ expect(handleChange).toHaveBeenCalledTimes(3);
+
+ // 尝试选择第四个节点,应该被阻止
+ fireEvent.click(londonNode);
+ expect(handleChange).toHaveBeenCalledTimes(3);
+
+ // 验证禁用状态
+ expect(londonNode.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
+ });
+
+ it('should handle maxCount with mixed selection strategies', () => {
+ const handleChange = jest.fn();
+
+ const { getByRole } = render(
+ ,
+ );
+
+ const container = getByRole('tree');
+
+ const tokyoNode = within(container).getByText('Tokyo');
+ fireEvent.click(tokyoNode);
+
+ // because UK node will show two children, so it will trigger one change
+ expect(handleChange).toHaveBeenCalledTimes(1);
+
+ const beijingNode = within(container).getByText('Beijing');
+ fireEvent.click(beijingNode);
+
+ // should not trigger change
+ expect(handleChange).toHaveBeenCalledTimes(1);
+ expect(beijingNode.closest('div')).toHaveClass('rc-tree-select-tree-treenode-disabled');
+ });
+});