Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TreeSelect support maxCount #596

Merged
merged 38 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fb8e621
feat: TreeSelect support maxCount
aojunhao123 Nov 9, 2024
95c7747
feat: sync activeKey state
aojunhao123 Nov 9, 2024
aa28c56
feat: sync disabled state
aojunhao123 Nov 9, 2024
8d63a07
feat: sync disabled state
aojunhao123 Nov 10, 2024
baf5f26
docs: improve maxCount demo
aojunhao123 Nov 10, 2024
828b5ee
test: add maxCount test cases
aojunhao123 Nov 10, 2024
ca8d966
chore: remove deadCode
aojunhao123 Nov 11, 2024
751e850
Merge branch 'master' into feat-maxCount
aojunhao123 Nov 11, 2024
cc3b839
test: add test cases for keyboard operations
aojunhao123 Nov 11, 2024
5039213
chore: remove useless code
aojunhao123 Nov 11, 2024
bd6b5ff
test: add test case
aojunhao123 Nov 11, 2024
15472c5
test: improve test case
aojunhao123 Nov 11, 2024
043527f
docs: add maxCount description
aojunhao123 Nov 11, 2024
182add5
feat: forbid check when checkedKeys more than maxCount
aojunhao123 Nov 11, 2024
ac9010c
chore: demo improvement
aojunhao123 Nov 11, 2024
c77d585
feat: adjust maxCount implement logic
aojunhao123 Nov 13, 2024
3559f12
fix: lint fix
aojunhao123 Nov 13, 2024
c72c25f
test: add test cases for maxCount
aojunhao123 Nov 13, 2024
c178539
chore: hoist state to context
aojunhao123 Nov 14, 2024
e9b59f9
chore: hoist traverse operation to TreeSelect
aojunhao123 Nov 14, 2024
feb012d
feat: improve keyboard navigation when reach maxCount
aojunhao123 Nov 20, 2024
06ee328
feat: improve keyboard navigation when reach maxCount
aojunhao123 Nov 20, 2024
ff9f216
perf: use cache to improve navigation performance
aojunhao123 Nov 21, 2024
a5625a1
refactor: reuse formatStrategyValues
aojunhao123 Nov 21, 2024
856be00
feat: add disabledStrategy
aojunhao123 Nov 22, 2024
e489e17
feat: add code comment
aojunhao123 Nov 22, 2024
06dee72
test: supplement test case for keyboard operation
aojunhao123 Nov 23, 2024
667dc46
chore: handle git conflicts manually
aojunhao123 Nov 23, 2024
a76ac30
Merge branch 'master' into feat-maxCount
aojunhao123 Nov 23, 2024
f8e5f61
chore: remove useless code
aojunhao123 Nov 23, 2024
7df686a
chore: memories displayValues
aojunhao123 Nov 25, 2024
c69fcae
refactor: use InternalContext
aojunhao123 Nov 27, 2024
f352161
chore: adjust context api
aojunhao123 Dec 2, 2024
2bd8a5c
chore: bump rc-tree version to 5.11.0 for support maxCount
aojunhao123 Dec 2, 2024
142385a
fix: test coverage
aojunhao123 Dec 2, 2024
73e9ae7
fix: fix some case
aojunhao123 Dec 3, 2024
f737832
chore: remove keyboard operation logic
aojunhao123 Dec 3, 2024
4c23314
chore: optimized code logic
aojunhao123 Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 65 additions & 65 deletions README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/demo/mutiple-with-maxCount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: mutiple-with-maxCount
nav:
title: Demo
path: /demo
---

<code src="../../examples/mutiple-with-maxCount.tsx"></code>
94 changes: 94 additions & 0 deletions examples/mutiple-with-maxCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { useState } from 'react';
import TreeSelect from '../src';

export default () => {
const [value, setValue] = useState<string[]>(['1']);
const [checkValue, setCheckValue] = useState<string[]>(['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 (
<>
<h2>multiple with maxCount</h2>
<TreeSelect
style={{ width: 300 }}
fieldNames={{ value: 'value', label: 'title' }}
multiple
maxCount={3}
treeData={treeData}
/>

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

<h2>checkable with maxCount and treeCheckStrictly</h2>
<TreeSelect
style={{ width: 300 }}
multiple
treeCheckable
treeCheckStrictly
maxCount={3}
treeData={treeData}
onChange={onCheckChange}
value={checkValue}
/>
</>
);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
91 changes: 53 additions & 38 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -45,6 +47,8 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
treeExpandAction,
treeTitleRender,
onPopupScroll,
displayValues,
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved
isOverMaxCount,
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved
} = React.useContext(TreeSelectContext);

const {
Expand Down Expand Up @@ -76,6 +80,11 @@ 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 @@ -154,6 +163,10 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
// 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<any>[]): EventDataNode<any> | null => {
for (const node of nodes) {
Expand Down Expand Up @@ -221,8 +234,9 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
// >>> 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),
Expand Down Expand Up @@ -276,42 +290,43 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
{activeEntity.node.value}
</span>
)}

<Tree
ref={treeRef}
focusable={false}
prefixCls={`${prefixCls}-tree`}
treeData={memoTreeData}
height={listHeight}
itemHeight={listItemHeight}
itemScrollOffset={listItemScrollOffset}
virtual={virtual !== false && dropdownMatchSelectWidth !== false}
multiple={multiple}
icon={treeIcon}
showIcon={showTreeIcon}
switcherIcon={switcherIcon}
showLine={treeLine}
loadData={syncLoadData}
motion={treeMotion}
activeKey={activeKey}
// We handle keys by out instead tree self
checkable={checkable}
checkStrictly
checkedKeys={mergedCheckedKeys}
selectedKeys={!checkable ? checkedKeys : []}
defaultExpandAll={treeDefaultExpandAll}
titleRender={treeTitleRender}
{...treeProps}
// Proxy event out
onActiveChange={setActiveKey}
onSelect={onInternalSelect}
onCheck={onInternalSelect}
onExpand={onInternalExpand}
onLoad={onTreeLoad}
filterTreeNode={filterTreeNode}
expandAction={treeExpandAction}
onScroll={onPopupScroll}
/>
<UnstableContext.Provider value={{ nodeDisabled }}>
<Tree
ref={treeRef}
focusable={false}
prefixCls={`${prefixCls}-tree`}
treeData={memoTreeData}
height={listHeight}
itemHeight={listItemHeight}
itemScrollOffset={listItemScrollOffset}
virtual={virtual !== false && dropdownMatchSelectWidth !== false}
multiple={multiple}
icon={treeIcon}
showIcon={showTreeIcon}
switcherIcon={switcherIcon}
showLine={treeLine}
loadData={syncLoadData}
motion={treeMotion}
activeKey={activeKey}
// We handle keys by out instead tree self
checkable={checkable}
checkStrictly
checkedKeys={mergedCheckedKeys}
selectedKeys={!checkable ? checkedKeys : []}
defaultExpandAll={treeDefaultExpandAll}
titleRender={treeTitleRender}
{...treeProps}
// Proxy event out
onActiveChange={setActiveKey}
onSelect={onInternalSelect}
onCheck={onInternalSelect}
onExpand={onInternalExpand}
onLoad={onTreeLoad}
filterTreeNode={filterTreeNode}
expandAction={treeExpandAction}
onScroll={onPopupScroll}
/>
</UnstableContext.Provider>
</div>
);
};
Expand Down
64 changes: 41 additions & 23 deletions src/TreeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface TreeSelectProps<ValueType = any, OptionType extends DataNode =
treeCheckable?: boolean | React.ReactNode;
treeCheckStrictly?: boolean;
labelInValue?: boolean;
maxCount?: number;

// >>> Data
treeData?: OptionType[];
Expand Down Expand Up @@ -136,6 +137,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
treeCheckable,
treeCheckStrictly,
labelInValue,
maxCount,
zombieJ marked this conversation as resolved.
Show resolved Hide resolved

// FieldNames
fieldNames,
Expand Down Expand Up @@ -413,6 +415,20 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((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;
}
}
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved

const labeledValues = convert2LabelValues(newRawValues);
setInternalValue(labeledValues);

Expand All @@ -425,12 +441,6 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((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;
Expand Down Expand Up @@ -558,6 +568,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
onDeselect,
rawCheckedValues,
rawHalfCheckedValues,
maxCount,
],
);

Expand Down Expand Up @@ -596,8 +607,11 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
});

// ========================== Context ===========================
const treeSelectContext = React.useMemo<TreeSelectContextProps>(
() => ({
const isOverMaxCount =
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved
mergedMultiple && maxCount !== undefined && cachedDisplayValues?.length >= maxCount;

const treeSelectContext = React.useMemo<TreeSelectContextProps>(() => {
return {
virtual,
dropdownMatchSelectWidth,
listHeight,
Expand All @@ -609,21 +623,25 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((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(
Expand Down
4 changes: 3 additions & 1 deletion src/TreeSelectContext.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +14,8 @@ export interface TreeSelectContextProps {
treeExpandAction?: ExpandAction;
treeTitleRender?: (node: any) => React.ReactNode;
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
displayValues?: LabeledValueType[];
isOverMaxCount?: boolean;
}

const TreeSelectContext = React.createContext<TreeSelectContextProps>(null as any);
Expand Down
Loading
Loading