Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 204
feat: TreeSelect support maxCount#596
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
fb8e62195c7747aa28c568d63a07baf5f26828b5eeca8d966751e850cc3b8395039213bd6b5ff15472c5043527f182add5ac9010cc77d5853559f12c72c25fc178539e9b59f9feb012d06ee328ff9f216a5625a1856be00e489e1706dee72667dc46a76ac30f8e5f617df686ac69fcaef3521612bd8a5c142385a73e9ae7f7378324c23314File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| 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> |
| 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} | ||
| /> | ||
| </> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| @@ -45,6 +47,8 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_, | ||
| treeExpandAction, | ||
| treeTitleRender, | ||
| onPopupScroll, | ||
| displayValues, | ||
| isOverMaxCount, | ||
aojunhao123 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } = React.useContext(TreeSelectContext); | ||
| const { | ||
| @@ -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) { | ||
| @@ -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) { | ||
| @@ -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), | ||
| @@ -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> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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[]; | ||
| @@ -136,6 +137,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref) | ||
| treeCheckable, | ||
| treeCheckStrictly, | ||
| labelInValue, | ||
| maxCount, | ||
zombieJ marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // FieldNames | ||
| fieldNames, | ||
| @@ -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.
Uh oh!There was an error while loading. Please reload this page. | ||
| const labeledValues = convert2LabelValues(newRawValues); | ||
| setInternalValue(labeledValues); | ||
| @@ -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; | ||
| @@ -558,6 +568,7 @@ const TreeSelect = React.forwardRef<BaseSelectRef, TreeSelectProps>((props, ref) | ||
| onDeselect, | ||
| rawCheckedValues, | ||
| rawHalfCheckedValues, | ||
| maxCount, | ||
| ], | ||
| ); | ||
| @@ -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.
Uh oh!There was an error while loading. Please reload this page. | ||
| mergedMultiple && maxCount !== undefined && cachedDisplayValues?.length >= maxCount; | ||
| const treeSelectContext = React.useMemo<TreeSelectContextProps>(() => { | ||
| return { | ||
| virtual, | ||
| dropdownMatchSelectWidth, | ||
| listHeight, | ||
| @@ -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( | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.