Skip to content

Commit

Permalink
feat: supplement maxCount logic for complicated cases (#602)
Browse files Browse the repository at this point in the history
* feat: supplement maxCount logic for complicated cases

* chore: remove unreachable logic

* feat: supplement maxCount logic for complicated cases

* fix: lint fix

* chore: remove console.log

* chore: remove conductCheck

* chore: change limit maxCount

* chore: update maxCount logic

* chore: add warning

* chore: cache cal

* fix: optimize null check logic

* chore: enhance warning message

* test: add warnings for maxCount

---------

Co-authored-by: 二货机器人 <[email protected]>
  • Loading branch information
aojunhao123 and zombieJ authored Dec 24, 2024
1 parent 78fdfa5 commit b858a0c
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 56 deletions.
20 changes: 15 additions & 5 deletions examples/mutiple-with-maxCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ export default () => {
key: '1-2',
value: '1-2',
title: '1-2',
disabled: true,
children: [
{
key: '1-2-1',
value: '1-2-1',
title: '1-2-1',
disabled: true,
},
{
key: '1-2-2',
value: '1-2-2',
title: '1-2-2',
},
],
},
{
key: '1-3',
Expand Down Expand Up @@ -63,21 +77,17 @@ export default () => {
maxCount={3}
treeData={treeData}
/>

<h2>checkable with maxCount</h2>
<TreeSelect
style={{ width: 300 }}
multiple
treeCheckable
// showCheckedStrategy="SHOW_ALL"
showCheckedStrategy="SHOW_PARENT"
// showCheckedStrategy="SHOW_CHILD"
// showCheckedStrategy="SHOW_PARENT"
maxCount={4}
treeData={treeData}
onChange={onChange}
value={value}
/>

<h2>checkable with maxCount and treeCheckStrictly</h2>
<TreeSelect
style={{ width: 300 }}
Expand Down
64 changes: 56 additions & 8 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
treeExpandAction,
treeTitleRender,
onPopupScroll,
displayValues,
isOverMaxCount,
leftMaxCount,
leafCountOnly,
valueEntities,
} = React.useContext(TreeSelectContext);

const {
Expand Down Expand Up @@ -80,11 +81,6 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
(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) {
Expand Down Expand Up @@ -163,8 +159,60 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);

// ========================= Disabled =========================
const disabledCacheRef = React.useRef<Map<string, boolean>>(new Map());

// Clear cache if `leftMaxCount` changed
React.useEffect(() => {
if (leftMaxCount) {
disabledCacheRef.current.clear();
}
}, [leftMaxCount]);

function getDisabledWithCache(node: DataNode) {
const value = node[fieldNames.value];
if (!disabledCacheRef.current.has(value)) {
const entity = valueEntities.get(value);
const isLeaf = (entity.children || []).length === 0;

if (!isLeaf) {
const checkableChildren = entity.children.filter(
childTreeNode =>
!childTreeNode.node.disabled &&
!childTreeNode.node.disableCheckbox &&
!checkedKeys.includes(childTreeNode.node[fieldNames.value]),
);

const checkableChildrenCount = checkableChildren.length;
disabledCacheRef.current.set(value, checkableChildrenCount > leftMaxCount);
} else {
disabledCacheRef.current.set(value, false);
}
}
return disabledCacheRef.current.get(value);
}

const nodeDisabled = useEvent((node: DataNode) => {
return isOverMaxCount && !memoRawValues.includes(node[fieldNames.value]);
const nodeValue = node[fieldNames.value];

if (checkedKeys.includes(nodeValue)) {
return false;
}

if (leftMaxCount === null) {
return false;
}

if (leftMaxCount <= 0) {
return true;
}

// This is a low performance calculation
if (leafCountOnly && leftMaxCount) {
return getDisabledWithCache(node);
}

return false;
});

// ========================== Get First Selectable Node ==========================
Expand Down
35 changes: 23 additions & 12 deletions src/TreeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,17 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)

const [cachedDisplayValues] = useCache(displayValues);

// ========================== MaxCount ==========================
const mergedMaxCount = React.useMemo(() => {
if (
mergedMultiple &&
(mergedShowCheckedStrategy === 'SHOW_CHILD' || treeCheckStrictly || !treeCheckable)
) {
return maxCount;
}
return null;
}, [maxCount, mergedMultiple, treeCheckStrictly, mergedShowCheckedStrategy, treeCheckable]);

// =========================== Change ===========================
const triggerChange = useRefFunc(
(
Expand All @@ -422,11 +433,9 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
mergedFieldNames,
);

// if multiple and maxCount is set, check if exceed maxCount
if (mergedMultiple && maxCount !== undefined) {
if (formattedKeyList.length > maxCount) {
return;
}
// Not allow pass with `maxCount`
if (mergedMaxCount && formattedKeyList.length > mergedMaxCount) {
return;
}

const labeledValues = convert2LabelValues(newRawValues);
Expand Down Expand Up @@ -607,9 +616,6 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
});

// ========================== Context ===========================
const isOverMaxCount =
mergedMultiple && maxCount !== undefined && cachedDisplayValues?.length >= maxCount;

const treeSelectContext = React.useMemo<TreeSelectContextProps>(() => {
return {
virtual,
Expand All @@ -623,8 +629,10 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
treeExpandAction,
treeTitleRender,
onPopupScroll,
displayValues: cachedDisplayValues,
isOverMaxCount,
leftMaxCount: maxCount === undefined ? null : maxCount - cachedDisplayValues.length,
leafCountOnly:
mergedShowCheckedStrategy === 'SHOW_CHILD' && !treeCheckStrictly && !!treeCheckable,
valueEntities,
};
}, [
virtual,
Expand All @@ -639,8 +647,11 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
treeTitleRender,
onPopupScroll,
maxCount,
cachedDisplayValues,
mergedMultiple,
cachedDisplayValues.length,
mergedShowCheckedStrategy,
treeCheckStrictly,
treeCheckable,
valueEntities,
]);

// ======================= Legacy Context =======================
Expand Down
11 changes: 8 additions & 3 deletions src/TreeSelectContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import type { ExpandAction } from 'rc-tree/lib/Tree';
import type { DataNode, FieldNames, Key, LabeledValueType } from './interface';
import type { DataNode, FieldNames, Key } from './interface';
import type useDataEntities from './hooks/useDataEntities';

export interface TreeSelectContextProps {
virtual?: boolean;
Expand All @@ -14,8 +15,12 @@ export interface TreeSelectContextProps {
treeExpandAction?: ExpandAction;
treeTitleRender?: (node: any) => React.ReactNode;
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
displayValues?: LabeledValueType[];
isOverMaxCount?: boolean;

// For `maxCount` usage
leftMaxCount: number | null;
/** When `true`, only take leaf node as count, or take all as count with `maxCount` limitation */
leafCountOnly: boolean;
valueEntities: ReturnType<typeof useDataEntities>['valueEntities'];
}

const TreeSelectContext = React.createContext<TreeSelectContextProps>(null as any);
Expand Down
15 changes: 14 additions & 1 deletion src/utils/warningPropsUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) {
labelInValue,
value,
multiple,
showCheckedStrategy,
maxCount,
} = props;

warning(!searchPlaceholder, '`searchPlaceholder` has been removed.');
Expand All @@ -20,7 +22,7 @@ function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) {

if (labelInValue || treeCheckStrictly) {
warning(
toArray(value).every((val) => val && typeof val === 'object' && 'value' in val),
toArray(value).every(val => val && typeof val === 'object' && 'value' in val),
'Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.',
);
}
Expand All @@ -33,6 +35,17 @@ function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) {
} else {
warning(!Array.isArray(value), '`value` should not be array when `TreeSelect` is single mode.');
}

if (
maxCount &&
((showCheckedStrategy === 'SHOW_ALL' && !treeCheckStrictly) ||
showCheckedStrategy === 'SHOW_PARENT')
) {
warning(
false,
'`maxCount` not work with `showCheckedStrategy=SHOW_ALL` (when `treeCheckStrictly=false`) or `showCheckedStrategy=SHOW_PARENT`.',
);
}
}

export default warningProps;
Loading

0 comments on commit b858a0c

Please sign in to comment.