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 12 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
129 changes: 64 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>
59 changes: 59 additions & 0 deletions examples/mutiple-with-maxCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useState } from 'react';
import TreeSelect from '../src';

export default () => {
const [value, setValue] = useState<string[]>(['1']);

const treeData = [
{
key: '1',
value: '1',
title: '1',
children: [
{
key: '1-1',
value: '1-1',
title: '1-1',
},
],
},
{
key: '2',
value: '2',
title: '2',
},
{
key: '3',
value: '3',
title: '3',
},
{
key: '4',
value: '4',
title: '4',
},
];

const onChange = (val: string[]) => {
setValue(val);
};

return (
<>
<h2>multiple with maxCount</h2>
<TreeSelect style={{ width: 300 }} multiple maxCount={3} treeData={treeData} />

<h2>checkable with maxCount</h2>
<TreeSelect
style={{ width: 300 }}
multiple
treeCheckable
treeCheckStrictly
maxCount={3}
treeData={treeData}
onChange={onChange}
value={value}
/>
</>
);
};
36 changes: 32 additions & 4 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as React from 'react';
import LegacyContext from './LegacyContext';
import TreeSelectContext from './TreeSelectContext';
import type { Key, SafeKey } from './interface';
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
import { getAllKeys, isCheckDisabled, isValidCount } from './utils/valueUtil';

const HIDDEN_STYLE = {
width: 0,
Expand Down Expand Up @@ -45,6 +45,7 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
treeExpandAction,
treeTitleRender,
onPopupScroll,
maxCount,
} = React.useContext(TreeSelectContext);

const {
Expand Down Expand Up @@ -76,6 +77,24 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
(prev, next) => next[0] && prev[1] !== next[1],
);

const isOverMaxCount = React.useMemo<boolean>(
() => multiple && isValidCount(maxCount) && checkedKeys.length >= maxCount,
[checkedKeys, maxCount, multiple],
);

const traverse = (nodes: EventDataNode<any>[]): EventDataNode<any>[] => {
return nodes.map(node => ({
...node,
disabled: isOverMaxCount && !checkedKeys.includes(node.key as SafeKey) ? true : node.disabled,
children: node.children ? traverse(node.children) : undefined,
}));
};

const processedTreeData = React.useMemo(
() => traverse(memoTreeData),
[memoTreeData, isOverMaxCount, checkedKeys],
);

// ========================== Values ==========================
const mergedCheckedKeys = React.useMemo(() => {
if (!checkable) {
Expand Down Expand Up @@ -109,8 +128,10 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
return;
}

const isSelected = !checkedKeys.includes(node.key);

onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
selected: isSelected,
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved
});

if (!multiple) {
Expand Down Expand Up @@ -183,6 +204,13 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
const [activeKey, setActiveKey] = React.useState<Key>(null);
const activeEntity = keyEntities[activeKey as SafeKey];

const onActiveChange = (key: Key) => {
if (isOverMaxCount && !checkedKeys.includes(key)) {
return;
}
setActiveKey(key);
};

React.useEffect(() => {
if (!open) {
return;
Expand Down Expand Up @@ -279,7 +307,7 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
ref={treeRef}
focusable={false}
prefixCls={`${prefixCls}-tree`}
treeData={memoTreeData}
treeData={processedTreeData}
height={listHeight}
itemHeight={listItemHeight}
itemScrollOffset={listItemScrollOffset}
Expand All @@ -301,7 +329,7 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
titleRender={treeTitleRender}
{...treeProps}
// Proxy event out
onActiveChange={setActiveKey}
onActiveChange={onActiveChange}
aojunhao123 marked this conversation as resolved.
Show resolved Hide resolved
onSelect={onInternalSelect}
onCheck={onInternalSelect}
onExpand={onInternalExpand}
Expand Down
5 changes: 5 additions & 0 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 @@ -558,6 +560,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
onDeselect,
rawCheckedValues,
rawHalfCheckedValues,
maxCount,
],
);

Expand Down Expand Up @@ -609,6 +612,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
treeExpandAction,
treeTitleRender,
onPopupScroll,
maxCount,
}),
[
virtual,
Expand All @@ -622,6 +626,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref)
treeExpandAction,
treeTitleRender,
onPopupScroll,
maxCount,
],
);

Expand Down
1 change: 1 addition & 0 deletions src/TreeSelectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface TreeSelectContextProps {
treeExpandAction?: ExpandAction;
treeTitleRender?: (node: any) => React.ReactNode;
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
maxCount?: number;
}

const TreeSelectContext = React.createContext<TreeSelectContextProps>(null as any);
Expand Down
3 changes: 3 additions & 0 deletions src/utils/valueUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ export const getAllKeys = (treeData: DataNode[], fieldNames: FieldNames): SafeKe
};

export const isNil = (val: any): boolean => val === null || val === undefined;

export const isValidCount = (value?: number) =>
typeof value !== 'undefined' && !Number.isNaN(value);
8 changes: 8 additions & 0 deletions tests/Select.SearchInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading