From 62dc021e3e778aebf10213da03aa50f39d764b8e Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 9 Dec 2021 15:28:59 +0800 Subject: [PATCH 01/37] chore: init link --- examples/basic.tsx | 8 +- package.json | 2 +- src/{Context.tsx => LegacyContext.tsx} | 4 +- src/OptionList.tsx | 44 ++--- src/TreeSelect.tsx | 219 ++++++++++++++++++++++++- src/TreeSelectContext.ts | 14 ++ src/generate.tsx | 2 +- src/hooks/useTreeData.ts | 48 ++---- src/utils/valueUtil.ts | 22 +++ 9 files changed, 301 insertions(+), 62 deletions(-) rename src/{Context.tsx => LegacyContext.tsx} (90%) create mode 100644 src/TreeSelectContext.ts diff --git a/examples/basic.tsx b/examples/basic.tsx index 4a3b124b..5b1dc091 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -150,7 +150,7 @@ class Demo extends React.Component { } = this.state; return (
-

tree-select in dialog

+ {/*

tree-select in dialog

@@ -184,7 +184,7 @@ class Demo extends React.Component { />
- ) : null} + ) : null} */}

single select

-

single select (just select children)

+ {/*

single select (just select children)

- +
*/} ); } diff --git a/package.json b/package.json index f4ec0813..d76ae14b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~13.2.1", + "rc-select": "~14.0.0-alpha.3", "rc-tree": "~5.3.0", "rc-util": "^5.16.1" } diff --git a/src/Context.tsx b/src/LegacyContext.tsx similarity index 90% rename from src/Context.tsx rename to src/LegacyContext.tsx index 62aa25c0..5f55c24f 100644 --- a/src/Context.tsx +++ b/src/LegacyContext.tsx @@ -3,7 +3,7 @@ import type { IconType } from 'rc-tree/lib/interface'; import type { FlattenDataNode, Key, LegacyDataNode, RawValueType } from './interface'; import type { SkipType } from './hooks/useKeyValueMapping'; -interface ContextProps { +interface LegacyContextProps { checkable: boolean | React.ReactNode; checkedKeys: Key[]; halfCheckedKeys: Key[]; @@ -31,4 +31,4 @@ interface ContextProps { ) => FlattenDataNode; } -export const SelectContext = React.createContext(null); +export const LegacySelectContext = React.createContext(null); diff --git a/src/OptionList.tsx b/src/OptionList.tsx index e793e395..6f35c24c 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import KeyCode from 'rc-util/lib/KeyCode'; import useMemo from 'rc-util/lib/hooks/useMemo'; import type { RefOptionListProps } from 'rc-select/lib/OptionList'; +import { useBaseProps } from 'rc-select'; import type { TreeProps } from 'rc-tree'; import Tree from 'rc-tree'; import type { EventDataNode, ScrollTo } from 'rc-tree/lib/interface'; import type { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface'; -import { SelectContext } from './Context'; +import { SelectContext } from './LegacyContext'; +import TreeSelectContext from './TreeSelectContext'; +import { getAllKeys } from './utils/valueUtil'; const HIDDEN_STYLE = { width: 0, @@ -59,19 +62,18 @@ const OptionList: React.RefForwardingComponent< > = (props, ref) => { const { prefixCls, - height, - itemHeight, - virtual, - options, - flattenOptions, multiple, searchValue, onSelect, - onToggleOpen, + toggleOpen, open, notFoundContent, onMouseEnter, - } = props; + } = useBaseProps(); + + const { virtual, listHeight, listItemHeight, treeData, fieldNames } = + React.useContext(TreeSelectContext); + const { checkable, checkedKeys, @@ -96,9 +98,9 @@ const OptionList: React.RefForwardingComponent< const treeRef = React.useRef(); - const memoOptions = useMemo( - () => options, - [open, options], + const memoTreeData = useMemo( + () => treeData, + [open, treeData], (prev, next) => next[0] && prev[1] !== next[1], ); @@ -154,9 +156,9 @@ const OptionList: React.RefForwardingComponent< React.useEffect(() => { if (searchValue) { - setSearchExpandedKeys(flattenOptions.map(o => o.key)); + setSearchExpandedKeys(getAllKeys(treeData, fieldNames)); } - }, [searchValue]); + }, [!!searchValue]); const onInternalExpand = (keys: Key[]) => { setExpandedKeys(keys); @@ -181,7 +183,7 @@ const OptionList: React.RefForwardingComponent< } if (!multiple) { - onToggleOpen(false); + toggleOpen(false); } }; @@ -216,7 +218,7 @@ const OptionList: React.RefForwardingComponent< // >>> Close case KeyCode.ESC: { - onToggleOpen(false); + toggleOpen(false); } } }, @@ -224,7 +226,7 @@ const OptionList: React.RefForwardingComponent< })); // ========================== Render ========================== - if (memoOptions.length === 0) { + if (memoTreeData.length === 0) { return (
{notFoundContent} @@ -232,7 +234,9 @@ const OptionList: React.RefForwardingComponent< ); } - const treeProps: Partial = {}; + const treeProps: Partial = { + fieldNames, + }; if (treeLoadedKeys) { treeProps.loadedKeys = treeLoadedKeys; } @@ -252,9 +256,9 @@ const OptionList: React.RefForwardingComponent< ref={treeRef} focusable={false} prefixCls={`${prefixCls}-tree`} - treeData={memoOptions as TreeDataNode[]} - height={height} - itemHeight={itemHeight} + treeData={memoTreeData as TreeDataNode[]} + height={listHeight} + itemHeight={listItemHeight} virtual={virtual} multiple={multiple} icon={treeIcon} diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index d92fc201..9d44fa17 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -1,8 +1,221 @@ -import generate, { TreeSelectProps } from './generate'; +// import generate, { TreeSelectProps } from './generate'; +import * as React from 'react'; +import { BaseSelect } from 'rc-select'; +import type { BaseSelectRef, BaseSelectPropsWithoutPrivate } from 'rc-select'; +import useId from 'rc-select/lib/hooks/useId'; import OptionList from './OptionList'; +import TreeNode from './TreeNode'; +import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; +import TreeSelectContext from './TreeSelectContext'; +import LegacyContext from './LegacyContext'; +import useTreeData from './hooks/useTreeData'; +import { + flattenOptions, + filterOptions, + isValueDisabled, + findValueOption, + addValue, + removeValue, + getRawValueLabeled, + toArray, + fillFieldNames, +} from './utils/valueUtil'; -const TreeSelect = generate({ prefixCls: 'rc-tree-select', optionList: OptionList as any }); +export type RawValueType = string | number; -export { TreeSelectProps }; +export interface FieldNames { + value?: string; + label?: string; + children?: string; +} + +export interface SimpleModeConfig { + id?: React.Key; + pId?: React.Key; + rootPId?: React.Key; +} + +export interface BaseOptionType { + disabled?: boolean; + checkable?: boolean; + disableCheckbox?: boolean; + children?: BaseOptionType[]; + [name: string]: any; +} + +export interface DefaultOptionType extends BaseOptionType { + value?: RawValueType; + title?: React.ReactNode; + label?: React.ReactNode; + key?: React.Key; + children?: DefaultOptionType[]; +} + +export interface LegacyDataNode extends DefaultOptionType { + props: any; +} + +export interface TreeSelectProps + extends BaseSelectPropsWithoutPrivate { + prefixCls?: string; + id?: string; + + // >>> Field Names + fieldNames?: FieldNames; + + // >>> Mode + treeCheckable?: boolean | React.ReactNode; + treeCheckStrictly?: boolean; + + // >>> Data + treeData?: OptionType[]; + treeDataSimpleMode?: boolean | SimpleModeConfig; + loadData?: (dataNode: LegacyDataNode) => Promise; + + // >>> Options + virtual?: boolean; + listHeight?: number; + listItemHeight?: number; +} + +const TreeSelect = React.forwardRef((props, ref) => { + const { + id, + prefixCls = 'rc-tree-select', + + // >>> Mode + treeCheckable, + treeCheckStrictly, + + // FieldNames + fieldNames, + + // Data + treeDataSimpleMode, + treeData, + children, + loadData, + + // Options + virtual, + listHeight = 200, + listItemHeight = 20, + } = props; + + const mergedId = useId(id); + const mergedCheckable: React.ReactNode | boolean = treeCheckable || treeCheckStrictly; + const mergedFieldNames = React.useMemo(() => fillFieldNames(fieldNames, true), [fieldNames]); + + // =========================== Values =========================== + const displayValues = React.useMemo(() => [], []); + + // ========================== Options =========================== + // Legacy both support `label` or `title` if not set. + // We have to fallback to function to handle this + const getTreeNodeTitle = React.useCallback( + (node: DefaultOptionType): React.ReactNode => { + if (!treeData) { + return node.title; + } + + if (mergedFieldNames?.label) { + return node[mergedFieldNames.label]; + } + + return node.label || node.title; + }, + [mergedFieldNames, treeData], + ); + + const mergedTreeData = useTreeData(treeData, children, { + getLabelProp: getTreeNodeTitle, + simpleMode: treeDataSimpleMode, + fieldNames: mergedFieldNames, + }); + + // ========================== Context =========================== + const treeSelectContext = React.useMemo( + () => ({ + virtual, + listHeight, + listItemHeight, + treeData: mergedTreeData, + fieldNames: mergedFieldNames, + }), + [virtual, listHeight, listItemHeight, mergedTreeData, mergedFieldNames], + ); + + // ======================= Legacy Context ======================= + const legacyContext = React.useMemo( + () => ({ + checkable: mergedCheckable, + loadData, + // treeLoadedKeys, + // onTreeLoad, + // checkedKeys: rawValues, + // halfCheckedKeys: rawHalfCheckedKeys, + // treeDefaultExpandAll, + // treeExpandedKeys, + // treeDefaultExpandedKeys, + // onTreeExpand, + // treeIcon, + // treeMotion, + // showTreeIcon, + // switcherIcon, + // treeLine, + // treeNodeFilterProp, + // getEntityByKey, + // getEntityByValue, + }), + [ + mergedCheckable, + loadData, + // treeLoadedKeys, + // onTreeLoad, + // rawValues, + // rawHalfCheckedKeys, + // treeDefaultExpandAll, + // treeExpandedKeys, + // treeDefaultExpandedKeys, + // onTreeExpand, + // treeIcon, + // treeMotion, + // showTreeIcon, + // switcherIcon, + // treeLine, + // treeNodeFilterProp, + // getEntityByKey, + // getEntityByValue, + ], + ); + + // =========================== Render =========================== + return ( + + + >> MISC + id={mergedId} + prefixCls={prefixCls} + displayValues={displayValues} + // >>> Options + OptionList={OptionList} + /> + + + ); +}) as any; // TODO: handle this + +// Assign name for Debug +if (process.env.NODE_ENV !== 'production') { + TreeSelect.displayName = 'TreeSelect'; +} + +TreeSelect.TreeNode = TreeNode; +TreeSelect.SHOW_ALL = SHOW_ALL; +TreeSelect.SHOW_PARENT = SHOW_PARENT; +TreeSelect.SHOW_CHILD = SHOW_CHILD; export default TreeSelect; diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts new file mode 100644 index 00000000..ebc011be --- /dev/null +++ b/src/TreeSelectContext.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import type { DefaultOptionType, FieldNames } from './TreeSelect'; + +export interface TreeSelectContextProps { + virtual?: boolean; + listHeight: number; + listItemHeight: number; + treeData: DefaultOptionType[]; + fieldNames: FieldNames; +} + +const TreeSelectContext = React.createContext(null as any); + +export default TreeSelectContext; diff --git a/src/generate.tsx b/src/generate.tsx index 64d06c6c..2047516d 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -37,7 +37,7 @@ import { fillFieldNames, } from './utils/valueUtil'; import warningProps from './utils/warningPropsUtil'; -import { SelectContext } from './Context'; +import { SelectContext } from './LegacyContext'; import useTreeData from './hooks/useTreeData'; import useKeyValueMap from './hooks/useKeyValueMap'; import useKeyValueMapping from './hooks/useKeyValueMapping'; diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index 3102bf87..115e7473 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -128,36 +128,22 @@ export default function useTreeData( fieldNames: FieldNames; }, ): InternalDataEntity[] { - const cacheRef = React.useRef<{ - treeData?: DataNode[]; - children?: React.ReactNode; - formatTreeData?: InternalDataEntity[]; - }>({}); - - if (treeData) { - cacheRef.current.formatTreeData = - cacheRef.current.treeData === treeData - ? cacheRef.current.formatTreeData - : formatTreeData( - simpleMode - ? parseSimpleTreeData(treeData, { - id: 'id', - pId: 'pId', - rootPId: null, - ...(simpleMode !== true ? simpleMode : {}), - }) - : treeData, - getLabelProp, - fieldNames, - ); - - cacheRef.current.treeData = treeData; - } else { - cacheRef.current.formatTreeData = - cacheRef.current.children === children - ? cacheRef.current.formatTreeData - : formatTreeData(convertChildrenToData(children), getLabelProp, fieldNames); - } + return React.useMemo(() => { + if (treeData) { + return formatTreeData( + simpleMode + ? parseSimpleTreeData(treeData, { + id: 'id', + pId: 'pId', + rootPId: null, + ...(simpleMode !== true ? simpleMode : {}), + }) + : treeData, + getLabelProp, + fieldNames, + ); + } - return cacheRef.current.formatTreeData; + return formatTreeData(convertChildrenToData(children), getLabelProp, fieldNames); + }, [children, fieldNames, getLabelProp, simpleMode, treeData]); } diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 2d43e9a7..95df0ea7 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -1,3 +1,4 @@ +import type * as React from 'react'; import { flattenTreeData } from 'rc-tree/lib/utils/treeUtil'; import type { FlattenNode } from 'rc-tree/lib/interface'; import type { FilterFunc } from 'rc-select/lib/interface/generator'; @@ -14,6 +15,7 @@ import type { } from '../interface'; import { fillLegacyProps } from './legacyUtil'; import type { SkipType } from '../hooks/useKeyValueMapping'; +import type { DefaultOptionType } from '../TreeSelect'; type CompatibleDataNode = Omit; @@ -249,3 +251,23 @@ export function removeValue(rawValues: RawValueType[], value: RawValueType) { values.delete(value); return Array.from(values); } + +/** Loop fetch all the keys exist in the tree */ +export function getAllKeys(treeData: DefaultOptionType[], fieldNames: FieldNames) { + const keys: React.Key[] = []; + + function dig(list: DefaultOptionType[]) { + list.forEach(item => { + keys.push(item[fieldNames.value]); + + const children = item[fieldNames.children]; + if (children) { + dig(children); + } + }); + } + + dig(treeData); + + return keys; +} From 999748ca383e08f7cdae283927184ec6cc169081 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 9 Dec 2021 15:31:12 +0800 Subject: [PATCH 02/37] chore: load Data --- src/LegacyContext.tsx | 4 +++- src/OptionList.tsx | 4 ++-- src/TreeSelect.tsx | 12 ++++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/LegacyContext.tsx b/src/LegacyContext.tsx index 5f55c24f..b4c50b33 100644 --- a/src/LegacyContext.tsx +++ b/src/LegacyContext.tsx @@ -31,4 +31,6 @@ interface LegacyContextProps { ) => FlattenDataNode; } -export const LegacySelectContext = React.createContext(null); +const LegacySelectContext = React.createContext(null); + +export default LegacySelectContext; diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 6f35c24c..97ae0d29 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -7,7 +7,7 @@ import type { TreeProps } from 'rc-tree'; import Tree from 'rc-tree'; import type { EventDataNode, ScrollTo } from 'rc-tree/lib/interface'; import type { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface'; -import { SelectContext } from './LegacyContext'; +import LegacyContext from './LegacyContext'; import TreeSelectContext from './TreeSelectContext'; import { getAllKeys } from './utils/valueUtil'; @@ -94,7 +94,7 @@ const OptionList: React.RefForwardingComponent< getEntityByKey, getEntityByValue, - } = React.useContext(SelectContext); + } = React.useContext(LegacyContext); const treeRef = React.useRef(); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 9d44fa17..d93ae8e4 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -71,6 +71,8 @@ export interface TreeSelectProps Promise; + treeLoadedKeys?: React.Key[]; + onTreeLoad?: (loadedKeys: React.Key[]) => void; // >>> Options virtual?: boolean; @@ -95,6 +97,8 @@ const TreeSelect = React.forwardRef((props, ref) treeData, children, loadData, + treeLoadedKeys, + onTreeLoad, // Options virtual, @@ -150,8 +154,8 @@ const TreeSelect = React.forwardRef((props, ref) () => ({ checkable: mergedCheckable, loadData, - // treeLoadedKeys, - // onTreeLoad, + treeLoadedKeys, + onTreeLoad, // checkedKeys: rawValues, // halfCheckedKeys: rawHalfCheckedKeys, // treeDefaultExpandAll, @@ -170,8 +174,8 @@ const TreeSelect = React.forwardRef((props, ref) [ mergedCheckable, loadData, - // treeLoadedKeys, - // onTreeLoad, + treeLoadedKeys, + onTreeLoad, // rawValues, // rawHalfCheckedKeys, // treeDefaultExpandAll, From 309313318ce34f6fae9445b772ce95b8f27a8843 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 9 Dec 2021 17:34:32 +0800 Subject: [PATCH 03/37] chore: connect with displayValue --- package.json | 2 +- src/OptionList.tsx | 25 ++++--- src/TreeSelect.tsx | 144 ++++++++++++++++++++++++++++++++------- src/TreeSelectContext.ts | 4 +- src/hooks/useCache.ts | 47 +++++++++++++ src/hooks/useRefFunc.ts | 16 +++++ src/hooks/useTreeData.ts | 37 ++++------ src/utils/valueUtil.ts | 19 +++++- 8 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 src/hooks/useCache.ts create mode 100644 src/hooks/useRefFunc.ts diff --git a/package.json b/package.json index d76ae14b..5f885cf4 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-select": "~14.0.0-alpha.3", - "rc-tree": "~5.3.0", + "rc-tree": "~5.3.2", "rc-util": "^5.16.1" } } diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 97ae0d29..1c144978 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -105,15 +105,18 @@ const OptionList: React.RefForwardingComponent< ); // ========================== Values ========================== - const valueKeys = React.useMemo( - () => - checkedKeys.map(val => { - // We should keep disabled value entity here - const entity = getEntityByValue(val, undefined, true); - return entity ? entity.key : null; - }), - [checkedKeys, getEntityByValue], - ); + // const valueKeys = React.useMemo( + // () => + // checkedKeys.map(val => { + // // We should keep disabled value entity here + // const entity = getEntityByValue(val, undefined, true); + // return entity ? entity.key : null; + // }), + // [checkedKeys, getEntityByValue], + // ); + + // TODO: handle this + const valueKeys = []; const mergedCheckedKeys = React.useMemo(() => { if (!checkable) { @@ -189,7 +192,9 @@ const OptionList: React.RefForwardingComponent< // ========================= Keyboard ========================= const [activeKey, setActiveKey] = React.useState(null); - const activeEntity = getEntityByKey(activeKey); + // const activeEntity = getEntityByKey(activeKey); + // TODO: handle this + const activeEntity = null; React.useImperativeHandle(ref, () => ({ scrollTo: treeRef.current?.scrollTo, diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index d93ae8e4..33d26af1 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -2,7 +2,9 @@ import * as React from 'react'; import { BaseSelect } from 'rc-select'; import type { BaseSelectRef, BaseSelectPropsWithoutPrivate } from 'rc-select'; +import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; import useId from 'rc-select/lib/hooks/useId'; +import useMergedState from 'rc-util/lib/hooks/useMergedState'; import OptionList from './OptionList'; import TreeNode from './TreeNode'; import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; @@ -20,15 +22,30 @@ import { toArray, fillFieldNames, } from './utils/valueUtil'; +import useCache from './hooks/useCache'; export type RawValueType = string | number; +export interface LabeledValueType { + key?: React.Key; + value?: RawValueType; + label?: React.ReactNode; + /** Only works on `treeCheckStrictly` */ + halfChecked?: boolean; +} + +export type ValueType = RawValueType | LabeledValueType | (RawValueType | LabeledValueType)[]; + export interface FieldNames { value?: string; label?: string; children?: string; } +export interface InternalFieldName extends Omit { + _title: string[]; +} + export interface SimpleModeConfig { id?: React.Key; pId?: React.Key; @@ -60,6 +77,10 @@ export interface TreeSelectProps>> Value + value?: ValueType; + defaultValue?: ValueType; + // >>> Field Names fieldNames?: FieldNames; @@ -80,12 +101,20 @@ export interface TreeSelectProps((props, ref) => { const { id, prefixCls = 'rc-tree-select', - // >>> Mode + // Value + value, + defaultValue, + + // Mode treeCheckable, treeCheckStrictly, @@ -107,35 +136,102 @@ const TreeSelect = React.forwardRef((props, ref) } = props; const mergedId = useId(id); + const treeConduction = treeCheckable && !treeCheckStrictly; const mergedCheckable: React.ReactNode | boolean = treeCheckable || treeCheckStrictly; - const mergedFieldNames = React.useMemo(() => fillFieldNames(fieldNames, true), [fieldNames]); + const mergedFieldNames: InternalFieldName = React.useMemo( + () => fillFieldNames(fieldNames), + [fieldNames], + ); - // =========================== Values =========================== - const displayValues = React.useMemo(() => [], []); - - // ========================== Options =========================== - // Legacy both support `label` or `title` if not set. - // We have to fallback to function to handle this - const getTreeNodeTitle = React.useCallback( - (node: DefaultOptionType): React.ReactNode => { - if (!treeData) { - return node.title; - } + // ============================ Data ============================ + // `useTreeData` only do convert of `children` or `simpleMode`. + // Else will return origin `treeData` for perf consideration. + // Do not do anything to loop the data. + const mergedTreeData = useTreeData(treeData, children, treeDataSimpleMode); + + const { keyEntities } = React.useMemo( + () => + convertDataToEntities(mergedTreeData as any, { + fieldNames: mergedFieldNames, + }), + [mergedTreeData, mergedFieldNames], + ); + + // useCache(mergedTreeData, treeConduction, mergedFieldNames); - if (mergedFieldNames?.label) { - return node[mergedFieldNames.label]; + // =========================== Label ============================ + const getLabel = React.useCallback( + (item: DefaultOptionType) => { + if (item) { + const { _title: titleList } = mergedFieldNames; + + for (let i = 0; i < titleList.length; i += 1) { + const title = item[titleList[i]]; + if (title !== undefined) { + return title; + } + } } + }, + [mergedFieldNames], + ); + + // ========================= Wrap Value ========================= + const convert2LabelValues = React.useCallback( + (draftValues: ValueType) => { + const values = toArray(draftValues); + + return values.map(val => { + let rawLabel: React.ReactNode; + let rawValue: RawValueType; + let rawHalfChecked: boolean; + + // Init provided info + if (!isRawValue(val)) { + rawLabel = val.label; + rawValue = val.value; + rawHalfChecked = val.halfChecked; + } else { + rawValue = val; + } - return node.label || node.title; + // Fill missing label + if (rawLabel === undefined) { + const entity = keyEntities[rawValue]; + rawLabel = getLabel(entity?.node); + } + + return { + label: rawLabel, + value: rawValue, + halfChecked: rawHalfChecked, + }; + }); }, - [mergedFieldNames, treeData], + [keyEntities, getLabel], + ); + + // =========================== Values =========================== + const [internalValue, setInternalValue] = useMergedState(defaultValue, { value }); + + const rawLabeledValues = React.useMemo( + () => convert2LabelValues(internalValue), + [convert2LabelValues, internalValue], + ); + + const displayValues = React.useMemo( + () => + rawLabeledValues.map(item => ({ + ...item, + label: item.label ?? item.value, + })), + [rawLabeledValues], ); - const mergedTreeData = useTreeData(treeData, children, { - getLabelProp: getTreeNodeTitle, - simpleMode: treeDataSimpleMode, - fieldNames: mergedFieldNames, - }); + const rawValues = React.useMemo( + () => rawLabeledValues.map(item => item.value), + [rawLabeledValues], + ); // ========================== Context =========================== const treeSelectContext = React.useMemo( @@ -156,7 +252,7 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - // checkedKeys: rawValues, + checkedKeys: rawValues, // halfCheckedKeys: rawHalfCheckedKeys, // treeDefaultExpandAll, // treeExpandedKeys, @@ -176,7 +272,7 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - // rawValues, + rawValues, // rawHalfCheckedKeys, // treeDefaultExpandAll, // treeExpandedKeys, diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts index ebc011be..fa3ff3f5 100644 --- a/src/TreeSelectContext.ts +++ b/src/TreeSelectContext.ts @@ -1,12 +1,12 @@ import * as React from 'react'; -import type { DefaultOptionType, FieldNames } from './TreeSelect'; +import type { DefaultOptionType, InternalFieldName } from './TreeSelect'; export interface TreeSelectContextProps { virtual?: boolean; listHeight: number; listItemHeight: number; treeData: DefaultOptionType[]; - fieldNames: FieldNames; + fieldNames: InternalFieldName; } const TreeSelectContext = React.createContext(null as any); diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts new file mode 100644 index 00000000..335d8de2 --- /dev/null +++ b/src/hooks/useCache.ts @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; +import type { DefaultOptionType } from '../TreeSelect'; +import useRefFunc from './useRefFunc'; + +/** + * This function will try to call requestIdleCallback if available to save performance. + */ +export default ( + treeData: DefaultOptionType[], + treeConduction: boolean, + fieldNames: InternalFieldName, +) => { + // const { keyEntities: conductKeyEntities } = React.useMemo(() => { + // if (treeConduction) { + // return convertDataToEntities(mergedTreeData as any); + // } + // return { keyEntities: null }; + // }, [mergedTreeData, treeCheckable, treeCheckStrictly]); + const cacheRef = React.useRef<{ + process?: DefaultOptionType[]; + treeData?: DefaultOptionType[]; + keyEntities?: Record; + }>({}); + + // Call useIdleCallback to get data async + React.useEffect(() => { + if (typeof requestIdleCallback !== 'undefined' && typeof cancelIdleCallback !== 'undefined') { + cacheRef.current.process = treeData; + + // Call requestIdleCallback + const id = requestIdleCallback(() => { + console.log('waht????'); + }); + + return () => cancelIdleCallback(id); + } + }, [treeData]); + + return useRefFunc(() => { + if (cacheRef.current.treeData !== treeData) { + // Force generate conduction data + } + + return cacheRef.current.keyEntities; + }); +}; diff --git a/src/hooks/useRefFunc.ts b/src/hooks/useRefFunc.ts new file mode 100644 index 00000000..720f972c --- /dev/null +++ b/src/hooks/useRefFunc.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +/** + * Same as `React.useCallback` but always return a memoized function + * but redirect to real function. + */ +export default function useRefFunc any>(callback: T): T { + const funcRef = React.useRef(); + funcRef.current = callback; + + const cacheFn = React.useCallback((...args: any[]) => { + return funcRef.current(...args); + }, []); + + return cacheFn as any; +} diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index 115e7473..8a6d8156 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -8,6 +8,7 @@ import type { FieldNames, } from '../interface'; import { convertChildrenToData } from '../utils/legacyUtil'; +import type { DefaultOptionType } from '../TreeSelect'; const MAX_WARNING_TIMES = 10; @@ -118,32 +119,20 @@ function formatTreeData( export default function useTreeData( treeData: DataNode[], children: React.ReactNode, - { - getLabelProp, - simpleMode, - fieldNames, - }: { - getLabelProp: (node: DataNode) => React.ReactNode; - simpleMode: boolean | SimpleModeConfig; - fieldNames: FieldNames; - }, -): InternalDataEntity[] { + simpleMode: boolean | SimpleModeConfig, +): DefaultOptionType[] { return React.useMemo(() => { if (treeData) { - return formatTreeData( - simpleMode - ? parseSimpleTreeData(treeData, { - id: 'id', - pId: 'pId', - rootPId: null, - ...(simpleMode !== true ? simpleMode : {}), - }) - : treeData, - getLabelProp, - fieldNames, - ); + return simpleMode + ? parseSimpleTreeData(treeData, { + id: 'id', + pId: 'pId', + rootPId: null, + ...(simpleMode !== true ? simpleMode : {}), + }) + : treeData; } - return formatTreeData(convertChildrenToData(children), getLabelProp, fieldNames); - }, [children, fieldNames, getLabelProp, simpleMode, treeData]); + return convertChildrenToData(children); + }, [children, simpleMode, treeData]); } diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 95df0ea7..49fa4f1f 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -15,7 +15,7 @@ import type { } from '../interface'; import { fillLegacyProps } from './legacyUtil'; import type { SkipType } from '../hooks/useKeyValueMapping'; -import type { DefaultOptionType } from '../TreeSelect'; +import type { DefaultOptionType, InternalFieldName } from '../TreeSelect'; type CompatibleDataNode = Omit; @@ -33,7 +33,7 @@ export function toArray(value: T | T[]): T[] { * @param skipTitle Skip if no need fill `title`. This is useful since we have 2 name as same title level * @returns */ -export function fillFieldNames(fieldNames?: FieldNames, skipTitle: boolean = false) { +export function fillFieldNames2333(fieldNames?: FieldNames, skipTitle: boolean = false) { const { label, value, children } = fieldNames || {}; const filledNames: FieldNames = { @@ -48,6 +48,19 @@ export function fillFieldNames(fieldNames?: FieldNames, skipTitle: boolean = fal return filledNames; } +export function fillFieldNames(fieldNames?: FieldNames) { + const { label, value, children } = fieldNames || {}; + + const mergedValue = value || 'value'; + + return { + _title: label ? [label] : ['title', 'label'], + value: mergedValue, + key: mergedValue, + children: children || 'children', + }; +} + export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] { const optionMap: Map = new Map(); @@ -253,7 +266,7 @@ export function removeValue(rawValues: RawValueType[], value: RawValueType) { } /** Loop fetch all the keys exist in the tree */ -export function getAllKeys(treeData: DefaultOptionType[], fieldNames: FieldNames) { +export function getAllKeys(treeData: DefaultOptionType[], fieldNames: InternalFieldName) { const keys: React.Key[] = []; function dig(list: DefaultOptionType[]) { From 9bf1c1f8526210ea5a19de590fa64530c049a6cd Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 10 Dec 2021 12:13:33 +0800 Subject: [PATCH 04/37] chore: back of selec --- examples/basic.tsx | 10 +- src/OptionList.tsx | 78 ++++++------ src/TreeSelect.tsx | 233 ++++++++++++++++++++++++++++++++--- src/TreeSelectContext.ts | 3 +- src/hooks/useDataEntities.ts | 20 +++ 5 files changed, 278 insertions(+), 66 deletions(-) create mode 100644 src/hooks/useDataEntities.ts diff --git a/examples/basic.tsx b/examples/basic.tsx index 5b1dc091..cfa5649f 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -185,7 +185,7 @@ class Demo extends React.Component {
) : null} */} -

single select

+ {/*

single select

- {/*

single select (just select children)

+

single select (just select children)

+ /> */}

check select

{ - console.log('Max Tag Rest Value:', valueList); + // console.log('Max Tag Rest Value:', valueList); return `${valueList.length} rest...`; }} /> -

labelInValue & show path

+ {/*

labelInValue & show path

{ - prefixCls: string; - id: string; - options: OptionsType; - flattenOptions: FlattenDataNode[]; - height: number; - itemHeight: number; - virtual?: boolean; - values: Set; - multiple: boolean; - open: boolean; - defaultActiveFirstOption?: boolean; - notFoundContent?: React.ReactNode; - menuItemSelectedIcon?: any; - childrenAsData: boolean; - searchValue: string; - - onSelect: (value: RawValueType, option: { selected: boolean }) => void; - onToggleOpen: (open?: boolean) => void; - /** Tell Select that some value is now active to make accessibility work */ - onActiveValue: (value: RawValueType, index: number) => void; - onScroll: React.UIEventHandler; - - onMouseEnter: () => void; + // prefixCls: string; + // id: string; + // options: OptionsType; + // flattenOptions: FlattenDataNode[]; + // height: number; + // itemHeight: number; + // virtual?: boolean; + // values: Set; + // multiple: boolean; + // open: boolean; + // defaultActiveFirstOption?: boolean; + // notFoundContent?: React.ReactNode; + // menuItemSelectedIcon?: any; + // childrenAsData: boolean; + // searchValue: string; + // onSelect: (value: RawValueType, option: { selected: boolean }) => void; + // onToggleOpen: (open?: boolean) => void; + // /** Tell Select that some value is now active to make accessibility work */ + // onActiveValue: (value: RawValueType, index: number) => void; + // onScroll: React.UIEventHandler; + // onMouseEnter: () => void; } type ReviseRefOptionListProps = Omit & { scrollTo: ScrollTo }; @@ -60,18 +58,10 @@ const OptionList: React.RefForwardingComponent< ReviseRefOptionListProps, OptionListProps > = (props, ref) => { - const { - prefixCls, - multiple, - searchValue, - onSelect, - toggleOpen, - open, - notFoundContent, - onMouseEnter, - } = useBaseProps(); - - const { virtual, listHeight, listItemHeight, treeData, fieldNames } = + const { prefixCls, multiple, searchValue, toggleOpen, open, notFoundContent, onMouseEnter } = + useBaseProps(); + + const { virtual, listHeight, listItemHeight, treeData, fieldNames, onSelect } = React.useContext(TreeSelectContext); const { @@ -177,13 +167,17 @@ const OptionList: React.RefForwardingComponent< event.preventDefault(); }; - const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { - const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); - if (entity !== null) { - onSelect(entity.data.value, { - selected: !checkedKeys.includes(entity.data.value), - }); - } + // const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { + const onInternalSelect = (_: Key[], info: TreeEventInfo) => { + // const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); + // if (entity !== null) { + // onSelect(entity.data.value, { + // selected: !checkedKeys.includes(entity.data.value), + // }); + // } + onSelect(info.node.key, { + selected: !checkedKeys.includes(info.node.key), + }); if (!multiple) { toggleOpen(false); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 33d26af1..1edf90e9 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -1,14 +1,16 @@ // import generate, { TreeSelectProps } from './generate'; import * as React from 'react'; import { BaseSelect } from 'rc-select'; -import type { BaseSelectRef, BaseSelectPropsWithoutPrivate } from 'rc-select'; -import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; +import type { BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps } from 'rc-select'; +import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; import useId from 'rc-select/lib/hooks/useId'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import OptionList from './OptionList'; import TreeNode from './TreeNode'; -import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; +import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; +import type { CheckedStrategy } from './utils/strategyUtil'; import TreeSelectContext from './TreeSelectContext'; +import type { TreeSelectContextProps } from './TreeSelectContext'; import LegacyContext from './LegacyContext'; import useTreeData from './hooks/useTreeData'; import { @@ -23,6 +25,11 @@ import { fillFieldNames, } from './utils/valueUtil'; import useCache from './hooks/useCache'; +import useRefFunc from './hooks/useRefFunc'; +import useChange from './hooks/useChange'; +import useDataEntities from './hooks/useDataEntities'; + +export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void; export type RawValueType = string | number; @@ -36,6 +43,29 @@ export interface LabeledValueType { export type ValueType = RawValueType | LabeledValueType | (RawValueType | LabeledValueType)[]; +/** @deprecated This is only used for legacy compatible. Not works on new code. */ +export interface LegacyCheckedNode { + pos: string; + node: React.ReactElement; + children?: LegacyCheckedNode[]; +} + +export interface ChangeEventExtra { + /** @deprecated Please save prev value by control logic instead */ + preValue: LabeledValueType[]; + triggerValue: RawValueType; + /** @deprecated Use `onSelect` or `onDeselect` instead. */ + selected?: boolean; + /** @deprecated Use `onSelect` or `onDeselect` instead. */ + checked?: boolean; + + // Not sure if exist user still use this. We have to keep but not recommend user to use + /** @deprecated This prop not work as react node anymore. */ + triggerNode: React.ReactElement; + /** @deprecated This prop not work as react node anymore. */ + allCheckedNodes: LegacyCheckedNode[]; +} + export interface FieldNames { value?: string; label?: string; @@ -71,22 +101,30 @@ export interface DefaultOptionType extends BaseOptionType { export interface LegacyDataNode extends DefaultOptionType { props: any; } - export interface TreeSelectProps - extends BaseSelectPropsWithoutPrivate { + extends Omit { prefixCls?: string; id?: string; // >>> Value value?: ValueType; defaultValue?: ValueType; + onChange?: (value: ValueType, labelList: React.ReactNode[], extra: ChangeEventExtra) => void; + + // >>> Select + onSelect?: SelectProps['onSelect']; + + // >>> Selector + showCheckedStrategy?: CheckedStrategy; // >>> Field Names fieldNames?: FieldNames; // >>> Mode + multiple?: boolean; treeCheckable?: boolean | React.ReactNode; treeCheckStrictly?: boolean; + labelInValue?: boolean; // >>> Data treeData?: OptionType[]; @@ -113,10 +151,17 @@ const TreeSelect = React.forwardRef((props, ref) // Value value, defaultValue, + onChange, + onSelect, + + // Selector + showCheckedStrategy, // Mode + multiple, treeCheckable, treeCheckStrictly, + labelInValue, // FieldNames fieldNames, @@ -137,10 +182,15 @@ const TreeSelect = React.forwardRef((props, ref) const mergedId = useId(id); const treeConduction = treeCheckable && !treeCheckStrictly; - const mergedCheckable: React.ReactNode | boolean = treeCheckable || treeCheckStrictly; + const mergedCheckable: boolean = !!(treeCheckable || treeCheckStrictly); + const mergedLabelInValue = treeCheckStrictly || labelInValue; + const mergedMultiple = mergedCheckable || multiple; + const mergedFieldNames: InternalFieldName = React.useMemo( () => fillFieldNames(fieldNames), - [fieldNames], + /* eslint-disable react-hooks/exhaustive-deps */ + [JSON.stringify(fieldNames)], + /* eslint-enable react-hooks/exhaustive-deps */ ); // ============================ Data ============================ @@ -149,15 +199,10 @@ const TreeSelect = React.forwardRef((props, ref) // Do not do anything to loop the data. const mergedTreeData = useTreeData(treeData, children, treeDataSimpleMode); - const { keyEntities } = React.useMemo( - () => - convertDataToEntities(mergedTreeData as any, { - fieldNames: mergedFieldNames, - }), - [mergedTreeData, mergedFieldNames], - ); + const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames); // useCache(mergedTreeData, treeConduction, mergedFieldNames); + // console.log('>>>', keyEntities); // =========================== Label ============================ const getLabel = React.useCallback( @@ -197,7 +242,7 @@ const TreeSelect = React.forwardRef((props, ref) // Fill missing label if (rawLabel === undefined) { - const entity = keyEntities[rawValue]; + const entity = valueEntities.get(rawValue); rawLabel = getLabel(entity?.node); } @@ -208,7 +253,7 @@ const TreeSelect = React.forwardRef((props, ref) }; }); }, - [keyEntities, getLabel], + [valueEntities, getLabel], ); // =========================== Values =========================== @@ -233,16 +278,167 @@ const TreeSelect = React.forwardRef((props, ref) [rawLabeledValues], ); + /** Get `missingRawValues` which not exist in the tree yet */ + const splitRawValues = React.useCallback( + (newRawValues: RawValueType[]) => { + const missingRawValues = []; + const existRawValues = []; + + // Keep missing value in the cache + newRawValues.forEach(val => { + if (valueEntities.has(val)) { + existRawValues.push(val); + } else { + missingRawValues.push(val); + } + }); + + return { missingRawValues, existRawValues }; + }, + [valueEntities], + ); + + // =========================== Change =========================== + console.log('Entities:', keyEntities, valueEntities); + const triggerChange = useRefFunc((newRawValues: RawValueType[]) => { + const labeledValues = convert2LabelValues(newRawValues); + setInternalValue(labeledValues); + + // Generate rest parameters is costly, so only do it when necessary + if (onChange) { + let eventValues: RawValueType[] = newRawValues; + if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') { + const keyList = newRawValues.map(val => { + const entity = valueEntities.get(val); + return entity?.key ?? val; + }); + const formattedKeyList = formatStrategyKeys(keyList, showCheckedStrategy, keyEntities); + eventValues = formattedKeyList.map(key => { + const entity = valueEntities.get(key); + return entity ? entity.node[mergedFieldNames.value] : key; + }); + } + + // const { triggerValue, selected } = extra || { + // triggerValue: undefined, + // selected: undefined, + // }; + + // let returnValues = mergedLabelInValue + // ? getRawValueLabeled(eventValues, value, getEntityByValue, getTreeNodeLabelProp) + // : eventValues; + + // // We need fill half check back + // if (treeCheckStrictly) { + // const halfValues = rawHalfCheckedKeys + // .map(key => { + // const entity = getEntityByKey(key); + // return entity ? entity.data.value : key; + // }) + // .filter(val => !eventValues.includes(val)); + + // returnValues = [ + // ...(returnValues as LabelValueType[]), + // ...getRawValueLabeled(halfValues, value, getEntityByValue, getTreeNodeLabelProp), + // ]; + // } + + // const additionalInfo = { + // // [Legacy] Always return as array contains label & value + // preValue: selectValues, + // triggerValue, + // } as ChangeEventExtra; + + // // [Legacy] Fill legacy data if user query. + // // This is expansive that we only fill when user query + // // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx + // let showPosition = true; + // if (treeCheckStrictly || (source === 'selection' && !selected)) { + // showPosition = false; + // } + + // fillAdditionalInfo(additionalInfo, triggerValue, newRawValues, mergedTreeData, showPosition); + + // if (mergedCheckable) { + // additionalInfo.checked = selected; + // } else { + // additionalInfo.selected = selected; + // } + + // onChange( + // mergedMultiple ? returnValues : returnValues[0], + // mergedLabelInValue + // ? null + // : eventValues.map(val => { + // const entity = getEntityByValue(val); + // return entity ? entity.data.title : null; + // }), + // additionalInfo, + // ); + } + }); + + // ========================== Options =========================== + /** Trigger by option list */ + const onOptionSelect: OnInternalSelect = React.useCallback( + (selectedKey, info) => { + const entity = keyEntities[selectedKey]; + + // const eventValue = mergedLabelInValue ? selectValue : selectValue; + // Never be falsy but keep it safe + if (entity) { + const selectedValue = entity.node[mergedFieldNames.value]; + + if (!mergedMultiple) { + // Single mode always set value + triggerChange([selectedValue]); + // triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source); + } else { + let newRawValues = Array.from([...rawValues, selectedValue]); + + // Add keys if tree conduction + if (treeConduction) { + // Should keep missing values + const { missingRawValues, existRawValues } = splitRawValues(newRawValues); + const keyList = existRawValues.map(val => valueEntities.get(val).key); + const { checkedKeys } = conductCheck(keyList, true, keyEntities); + newRawValues = [ + ...missingRawValues, + ...checkedKeys.map(key => keyEntities[key].node[mergedFieldNames.value]), + ]; + } + triggerChange(newRawValues); + // triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source); + } + + // Trigger select event + onSelect?.(selectedValue, entity.node); + } + }, + [ + splitRawValues, + valueEntities, + keyEntities, + mergedFieldNames, + mergedMultiple, + rawValues, + triggerChange, + treeConduction, + onSelect, + ], + ); + // ========================== Context =========================== - const treeSelectContext = React.useMemo( + const treeSelectContext = React.useMemo( () => ({ virtual, listHeight, listItemHeight, treeData: mergedTreeData, fieldNames: mergedFieldNames, + onSelect: onOptionSelect, }), - [virtual, listHeight, listItemHeight, mergedTreeData, mergedFieldNames], + [virtual, listHeight, listItemHeight, mergedTreeData, mergedFieldNames, onOptionSelect], ); // ======================= Legacy Context ======================= @@ -300,6 +496,7 @@ const TreeSelect = React.forwardRef((props, ref) id={mergedId} prefixCls={prefixCls} displayValues={displayValues} + mode={mergedMultiple ? 'multiple' : undefined} // >>> Options OptionList={OptionList} /> diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts index fa3ff3f5..2157044d 100644 --- a/src/TreeSelectContext.ts +++ b/src/TreeSelectContext.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { DefaultOptionType, InternalFieldName } from './TreeSelect'; +import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect'; export interface TreeSelectContextProps { virtual?: boolean; @@ -7,6 +7,7 @@ export interface TreeSelectContextProps { listItemHeight: number; treeData: DefaultOptionType[]; fieldNames: InternalFieldName; + onSelect: OnInternalSelect; } const TreeSelectContext = React.createContext(null as any); diff --git a/src/hooks/useDataEntities.ts b/src/hooks/useDataEntities.ts new file mode 100644 index 00000000..fe37fec4 --- /dev/null +++ b/src/hooks/useDataEntities.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; +import type { DataEntity } from 'rc-tree/lib/interface'; +import type { FieldNames } from '../TreeSelect'; + +export default (treeData: any, fieldNames: FieldNames) => + React.useMemo<{ keyEntities: Record }>( + () => + convertDataToEntities(treeData, { + fieldNames, + initWrapper: wrapper => ({ + ...wrapper, + valueEntities: new Map(), + }), + processEntity: (entity, wrapper: any) => { + wrapper.valueEntities.set(entity.node[fieldNames.value], entity); + }, + }), + [treeData, fieldNames], + ) as ReturnType & { valueEntities: Map }; From 77c67791558a17a3cc8184cb2f1598fd9fcbe6a6 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 10 Dec 2021 15:08:52 +0800 Subject: [PATCH 05/37] chore: back of onChange --- src/TreeSelect.tsx | 206 +++++++++++++++++++++------------------ src/hooks/useCache.ts | 60 +++++------- src/utils/legacyUtil.tsx | 22 +++-- 3 files changed, 151 insertions(+), 137 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 1edf90e9..0acf8019 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -28,6 +28,7 @@ import useCache from './hooks/useCache'; import useRefFunc from './hooks/useRefFunc'; import useChange from './hooks/useChange'; import useDataEntities from './hooks/useDataEntities'; +import { fillAdditionalInfo } from './utils/legacyUtil'; export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void; @@ -41,6 +42,8 @@ export interface LabeledValueType { halfChecked?: boolean; } +export type SelectSource = 'option' | 'selection' | 'input' | 'clear'; + export type ValueType = RawValueType | LabeledValueType | (RawValueType | LabeledValueType)[]; /** @deprecated This is only used for legacy compatible. Not works on new code. */ @@ -116,6 +119,7 @@ export interface TreeSelectProps>> Selector showCheckedStrategy?: CheckedStrategy; + treeNodeLabelProp?: string; // >>> Field Names fieldNames?: FieldNames; @@ -156,6 +160,7 @@ const TreeSelect = React.forwardRef((props, ref) // Selector showCheckedStrategy, + treeNodeLabelProp, // Mode multiple, @@ -201,13 +206,15 @@ const TreeSelect = React.forwardRef((props, ref) const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames); - // useCache(mergedTreeData, treeConduction, mergedFieldNames); - // console.log('>>>', keyEntities); - // =========================== Label ============================ const getLabel = React.useCallback( (item: DefaultOptionType) => { if (item) { + if (treeNodeLabelProp) { + return item[treeNodeLabelProp]; + } + + // Loop from fieldNames const { _title: titleList } = mergedFieldNames; for (let i = 0; i < titleList.length; i += 1) { @@ -218,7 +225,7 @@ const TreeSelect = React.forwardRef((props, ref) } } }, - [mergedFieldNames], + [mergedFieldNames, treeNodeLabelProp], ); // ========================= Wrap Value ========================= @@ -259,23 +266,37 @@ const TreeSelect = React.forwardRef((props, ref) // =========================== Values =========================== const [internalValue, setInternalValue] = useMergedState(defaultValue, { value }); - const rawLabeledValues = React.useMemo( + const rawMixedLabeledValues = React.useMemo( () => convert2LabelValues(internalValue), [convert2LabelValues, internalValue], ); + // Split value into full check and half check + const [rawLabeledValues, rawHalfCheckedValues] = React.useMemo(() => { + const fullCheckValues: LabeledValueType[] = []; + const halfCheckValues: LabeledValueType[] = []; + + rawMixedLabeledValues.forEach(item => { + if (item.halfChecked) { + halfCheckValues.push(item); + } else { + fullCheckValues.push(item); + } + }); + + return [fullCheckValues, halfCheckValues]; + }, [rawMixedLabeledValues]); + + const [mergedValues] = useCache(rawLabeledValues); + const rawValues = React.useMemo(() => mergedValues.map(item => item.value), [mergedValues]); + const displayValues = React.useMemo( () => - rawLabeledValues.map(item => ({ + mergedValues.map(item => ({ ...item, label: item.label ?? item.value, })), - [rawLabeledValues], - ); - - const rawValues = React.useMemo( - () => rawLabeledValues.map(item => item.value), - [rawLabeledValues], + [mergedValues], ); /** Get `missingRawValues` which not exist in the tree yet */ @@ -299,84 +320,85 @@ const TreeSelect = React.forwardRef((props, ref) ); // =========================== Change =========================== - console.log('Entities:', keyEntities, valueEntities); - const triggerChange = useRefFunc((newRawValues: RawValueType[]) => { - const labeledValues = convert2LabelValues(newRawValues); - setInternalValue(labeledValues); - - // Generate rest parameters is costly, so only do it when necessary - if (onChange) { - let eventValues: RawValueType[] = newRawValues; - if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') { - const keyList = newRawValues.map(val => { - const entity = valueEntities.get(val); - return entity?.key ?? val; - }); - const formattedKeyList = formatStrategyKeys(keyList, showCheckedStrategy, keyEntities); - eventValues = formattedKeyList.map(key => { - const entity = valueEntities.get(key); - return entity ? entity.node[mergedFieldNames.value] : key; - }); - } + const triggerChange = useRefFunc( + ( + newRawValues: RawValueType[], + extra: { triggerValue: RawValueType; selected: boolean }, + source: SelectSource, + ) => { + const labeledValues = convert2LabelValues(newRawValues); + setInternalValue(labeledValues); + + // Generate rest parameters is costly, so only do it when necessary + if (onChange) { + let eventValues: RawValueType[] = newRawValues; + if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') { + const keyList = newRawValues.map(val => { + const entity = valueEntities.get(val); + return entity?.key ?? val; + }); + const formattedKeyList = formatStrategyKeys(keyList, showCheckedStrategy, keyEntities); + eventValues = formattedKeyList.map(key => { + const entity = valueEntities.get(key); + return entity ? entity.node[mergedFieldNames.value] : key; + }); + } + + const { triggerValue, selected } = extra || { + triggerValue: undefined, + selected: undefined, + }; + + let returnLabeledValues: LabeledValueType[] = convert2LabelValues(eventValues); + + // We need fill half check back + if (treeCheckStrictly) { + const halfValues = rawHalfCheckedValues.filter(item => !eventValues.includes(item.value)); + + returnLabeledValues = [...returnLabeledValues, ...halfValues]; + } + + const additionalInfo = { + // [Legacy] Always return as array contains label & value + preValue: rawLabeledValues, + triggerValue, + } as ChangeEventExtra; + + // [Legacy] Fill legacy data if user query. + // This is expansive that we only fill when user query + // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx + let showPosition = true; + if (treeCheckStrictly || (source === 'selection' && !selected)) { + showPosition = false; + } - // const { triggerValue, selected } = extra || { - // triggerValue: undefined, - // selected: undefined, - // }; - - // let returnValues = mergedLabelInValue - // ? getRawValueLabeled(eventValues, value, getEntityByValue, getTreeNodeLabelProp) - // : eventValues; - - // // We need fill half check back - // if (treeCheckStrictly) { - // const halfValues = rawHalfCheckedKeys - // .map(key => { - // const entity = getEntityByKey(key); - // return entity ? entity.data.value : key; - // }) - // .filter(val => !eventValues.includes(val)); - - // returnValues = [ - // ...(returnValues as LabelValueType[]), - // ...getRawValueLabeled(halfValues, value, getEntityByValue, getTreeNodeLabelProp), - // ]; - // } - - // const additionalInfo = { - // // [Legacy] Always return as array contains label & value - // preValue: selectValues, - // triggerValue, - // } as ChangeEventExtra; - - // // [Legacy] Fill legacy data if user query. - // // This is expansive that we only fill when user query - // // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx - // let showPosition = true; - // if (treeCheckStrictly || (source === 'selection' && !selected)) { - // showPosition = false; - // } - - // fillAdditionalInfo(additionalInfo, triggerValue, newRawValues, mergedTreeData, showPosition); - - // if (mergedCheckable) { - // additionalInfo.checked = selected; - // } else { - // additionalInfo.selected = selected; - // } - - // onChange( - // mergedMultiple ? returnValues : returnValues[0], - // mergedLabelInValue - // ? null - // : eventValues.map(val => { - // const entity = getEntityByValue(val); - // return entity ? entity.data.title : null; - // }), - // additionalInfo, - // ); - } - }); + fillAdditionalInfo( + additionalInfo, + triggerValue, + newRawValues, + mergedTreeData, + showPosition, + fieldNames, + ); + + if (mergedCheckable) { + additionalInfo.checked = selected; + } else { + additionalInfo.selected = selected; + } + + const returnValues = returnLabeledValues + ? returnLabeledValues + : returnLabeledValues.map(item => item.value); + + onChange( + mergedMultiple ? returnValues : returnValues[0], + mergedLabelInValue ? null : returnLabeledValues.map(item => item.label), + additionalInfo, + ); + } + }, + ); // ========================== Options =========================== /** Trigger by option list */ @@ -391,8 +413,7 @@ const TreeSelect = React.forwardRef((props, ref) if (!mergedMultiple) { // Single mode always set value - triggerChange([selectedValue]); - // triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source); + triggerChange([selectedValue], { selected: true, triggerValue: selectedValue }, 'option'); } else { let newRawValues = Array.from([...rawValues, selectedValue]); @@ -407,8 +428,7 @@ const TreeSelect = React.forwardRef((props, ref) ...checkedKeys.map(key => keyEntities[key].node[mergedFieldNames.value]), ]; } - triggerChange(newRawValues); - // triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source); + triggerChange(newRawValues, { selected: true, triggerValue: selectedValue }, 'option'); } // Trigger select event diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts index 335d8de2..06b1defb 100644 --- a/src/hooks/useCache.ts +++ b/src/hooks/useCache.ts @@ -1,47 +1,35 @@ import * as React from 'react'; -import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; -import type { DefaultOptionType } from '../TreeSelect'; -import useRefFunc from './useRefFunc'; +import type { DataEntity } from 'rc-tree/lib/interface'; +import type { DefaultOptionType, LabeledValueType, RawValueType } from '../TreeSelect'; /** * This function will try to call requestIdleCallback if available to save performance. + * No need `getLabel` here since already fetch on `rawLabeledValue`. */ -export default ( - treeData: DefaultOptionType[], - treeConduction: boolean, - fieldNames: InternalFieldName, -) => { - // const { keyEntities: conductKeyEntities } = React.useMemo(() => { - // if (treeConduction) { - // return convertDataToEntities(mergedTreeData as any); - // } - // return { keyEntities: null }; - // }, [mergedTreeData, treeCheckable, treeCheckStrictly]); - const cacheRef = React.useRef<{ - process?: DefaultOptionType[]; - treeData?: DefaultOptionType[]; - keyEntities?: Record; - }>({}); +export default (values: LabeledValueType[]): [LabeledValueType[]] => { + const cacheRef = React.useRef({ + valueLabels: new Map(), + }); - // Call useIdleCallback to get data async - React.useEffect(() => { - if (typeof requestIdleCallback !== 'undefined' && typeof cancelIdleCallback !== 'undefined') { - cacheRef.current.process = treeData; + return React.useMemo(() => { + const { valueLabels } = cacheRef.current; + const valueLabelsCache = new Map(); - // Call requestIdleCallback - const id = requestIdleCallback(() => { - console.log('waht????'); - }); + const filledValues = values.map(item => { + const { value } = item; + const mergedLabel = item.label ?? valueLabels.get(value); - return () => cancelIdleCallback(id); - } - }, [treeData]); + // Save in cache + valueLabelsCache.set(value, mergedLabel); - return useRefFunc(() => { - if (cacheRef.current.treeData !== treeData) { - // Force generate conduction data - } + return { + ...item, + label: mergedLabel, + }; + }); - return cacheRef.current.keyEntities; - }); + cacheRef.current.valueLabels = valueLabelsCache; + + return [filledValues]; + }, [values]); }; diff --git a/src/utils/legacyUtil.tsx b/src/utils/legacyUtil.tsx index 878c53c2..d0a3efc4 100644 --- a/src/utils/legacyUtil.tsx +++ b/src/utils/legacyUtil.tsx @@ -5,11 +5,11 @@ import type { DataNode, LegacyDataNode, ChangeEventExtra, - InternalDataEntity, RawValueType, LegacyCheckedNode, } from '../interface'; import TreeNode from '../TreeNode'; +import type { DefaultOptionType, FieldNames } from '../TreeSelect'; export function convertChildrenToData(nodes: React.ReactNode): DataNode[] { return toArray(nodes) @@ -66,23 +66,29 @@ export function fillAdditionalInfo( extra: ChangeEventExtra, triggerValue: RawValueType, checkedValues: RawValueType[], - treeData: InternalDataEntity[], + treeData: DefaultOptionType[], showPosition: boolean, + fieldNames: FieldNames, ) { let triggerNode: React.ReactNode = null; let nodeList: LegacyCheckedNode[] = null; function generateMap() { - function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) { + function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) { return list - .map((dataNode, index) => { + .map((option, index) => { const pos = `${level}-${index}`; - const included = checkedValues.includes(dataNode.value); - const children = dig(dataNode.children || [], pos, included); - const node = {children.map(child => child.node)}; + const value = option[fieldNames.value]; + const included = checkedValues.includes(value); + const children = dig(option[fieldNames.children] || [], pos, included); + const node = ( + )}> + {children.map(child => child.node)} + + ); // Link with trigger node - if (triggerValue === dataNode.value) { + if (triggerValue === value) { triggerNode = node; } From e73fd1c99321448c148d1188777fcff79dcf8a1b Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 10 Dec 2021 16:54:56 +0800 Subject: [PATCH 06/37] chore: checked logic --- src/TreeSelect.tsx | 38 ++++++++++++++++++++++++++++++++++++-- src/utils/strategyUtil.ts | 9 +++++---- src/utils/valueUtil.ts | 2 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 0acf8019..3666cc1d 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -1,7 +1,12 @@ // import generate, { TreeSelectProps } from './generate'; import * as React from 'react'; import { BaseSelect } from 'rc-select'; -import type { BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps } from 'rc-select'; +import type { + BaseSelectRef, + BaseSelectPropsWithoutPrivate, + BaseSelectProps, + SelectProps, +} from 'rc-select'; import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; import useId from 'rc-select/lib/hooks/useId'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; @@ -114,6 +119,13 @@ export interface TreeSelectProps void; + // >>> Search + searchValue?: string; + /** @deprecated Use `searchValue` instead */ + inputValue?: string; + onSearch?: (value: string) => void; + autoClearSearchValue?: boolean; + // >>> Select onSelect?: SelectProps['onSelect']; @@ -158,8 +170,14 @@ const TreeSelect = React.forwardRef((props, ref) onChange, onSelect, + // Search + searchValue, + inputValue, + onSearch, + autoClearSearchValue = true, + // Selector - showCheckedStrategy, + showCheckedStrategy = SHOW_CHILD, treeNodeLabelProp, // Mode @@ -191,6 +209,7 @@ const TreeSelect = React.forwardRef((props, ref) const mergedLabelInValue = treeCheckStrictly || labelInValue; const mergedMultiple = mergedCheckable || multiple; + // ========================= FieldNames ========================= const mergedFieldNames: InternalFieldName = React.useMemo( () => fillFieldNames(fieldNames), /* eslint-disable react-hooks/exhaustive-deps */ @@ -198,6 +217,17 @@ const TreeSelect = React.forwardRef((props, ref) /* eslint-enable react-hooks/exhaustive-deps */ ); + // =========================== Search =========================== + const [mergedSearchValue, setSearchValue] = useMergedState('', { + value: searchValue !== undefined ? searchValue : inputValue, + postState: search => search || '', + }); + + const onInternalSearch: BaseSelectProps['onSearch'] = searchText => { + setSearchValue(searchText); + onSearch?.(searchText); + }; + // ============================ Data ============================ // `useTreeData` only do convert of `children` or `simpleMode`. // Else will return origin `treeData` for perf consideration. @@ -205,6 +235,7 @@ const TreeSelect = React.forwardRef((props, ref) const mergedTreeData = useTreeData(treeData, children, treeDataSimpleMode); const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames); + // console.log('KeyEntities', keyEntities); // =========================== Label ============================ const getLabel = React.useCallback( @@ -517,6 +548,9 @@ const TreeSelect = React.forwardRef((props, ref) prefixCls={prefixCls} displayValues={displayValues} mode={mergedMultiple ? 'multiple' : undefined} + // >>> Search + searchValue={mergedSearchValue} + onSearch={onInternalSearch} // >>> Options OptionList={OptionList} /> diff --git a/src/utils/strategyUtil.ts b/src/utils/strategyUtil.ts index 2ac256b6..02bd1896 100644 --- a/src/utils/strategyUtil.ts +++ b/src/utils/strategyUtil.ts @@ -1,4 +1,5 @@ import type { DataEntity } from 'rc-tree/lib/interface'; +import type * as React from 'react'; import type { RawValueType, Key, DataNode } from '../interface'; import { isCheckDisabled } from './valueUtil'; @@ -9,14 +10,14 @@ export const SHOW_CHILD = 'SHOW_CHILD'; export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD; export function formatStrategyKeys( - keys: Key[], + keys: React.Key[], strategy: CheckedStrategy, keyEntities: Record, ): RawValueType[] { const keySet = new Set(keys); if (strategy === SHOW_CHILD) { - return keys.filter((key) => { + return keys.filter(key => { const entity = keyEntities[key]; if ( @@ -32,11 +33,11 @@ export function formatStrategyKeys( }); } if (strategy === SHOW_PARENT) { - return keys.filter((key) => { + return keys.filter(key => { const entity = keyEntities[key]; const parent = entity ? entity.parent : null; - if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) { + if (parent && !isCheckDisabled(parent.node) && keySet.has(parent.key)) { return false; } return true; diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 49fa4f1f..e32c18a5 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -82,7 +82,7 @@ export function isValueDisabled(value: RawValueType, options: CompatibleDataNode } export function isCheckDisabled(node: DataNode) { - return node.disabled || node.disableCheckbox || node.checkable === false; + return !node || node.disabled || node.disableCheckbox || node.checkable === false; } interface TreeDataNode extends InternalDataEntity { From 010ae7b0f4d91fdf40a00d935c429f40f3ffa332 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 10 Dec 2021 17:56:18 +0800 Subject: [PATCH 07/37] chore: conduction tree data --- examples/basic.tsx | 1 + src/OptionList.tsx | 31 +++------------------ src/TreeSelect.tsx | 54 +++++++++++++++++++++--------------- src/hooks/useCheckedKeys.ts | 21 ++++++++++++++ src/hooks/useDataEntities.ts | 4 +-- 5 files changed, 59 insertions(+), 52 deletions(-) create mode 100644 src/hooks/useCheckedKeys.ts diff --git a/examples/basic.tsx b/examples/basic.tsx index cfa5649f..8a84f565 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -253,6 +253,7 @@ class Demo extends React.Component {

check select

(); @@ -95,35 +92,22 @@ const OptionList: React.RefForwardingComponent< ); // ========================== Values ========================== - // const valueKeys = React.useMemo( - // () => - // checkedKeys.map(val => { - // // We should keep disabled value entity here - // const entity = getEntityByValue(val, undefined, true); - // return entity ? entity.key : null; - // }), - // [checkedKeys, getEntityByValue], - // ); - - // TODO: handle this - const valueKeys = []; - const mergedCheckedKeys = React.useMemo(() => { if (!checkable) { return null; } return { - checked: valueKeys, + checked: checkedKeys, halfChecked: halfCheckedKeys, }; - }, [valueKeys, halfCheckedKeys, checkable]); + }, [checkable, checkedKeys, halfCheckedKeys]); // ========================== Scroll ========================== React.useEffect(() => { // Single mode should scroll to current key - if (open && !multiple && valueKeys.length) { - treeRef.current?.scrollTo({ key: valueKeys[0] }); + if (open && !multiple && checkedKeys.length) { + treeRef.current?.scrollTo({ key: checkedKeys[0] }); } }, [open]); @@ -167,14 +151,7 @@ const OptionList: React.RefForwardingComponent< event.preventDefault(); }; - // const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { const onInternalSelect = (_: Key[], info: TreeEventInfo) => { - // const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); - // if (entity !== null) { - // onSelect(entity.data.value, { - // selected: !checkedKeys.includes(entity.data.value), - // }); - // } onSelect(info.node.key, { selected: !checkedKeys.includes(info.node.key), }); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 3666cc1d..5d1546aa 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -34,6 +34,7 @@ import useRefFunc from './hooks/useRefFunc'; import useChange from './hooks/useChange'; import useDataEntities from './hooks/useDataEntities'; import { fillAdditionalInfo } from './utils/legacyUtil'; +import useCheckedKeys from './hooks/useCheckedKeys'; export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void; @@ -235,7 +236,26 @@ const TreeSelect = React.forwardRef((props, ref) const mergedTreeData = useTreeData(treeData, children, treeDataSimpleMode); const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames); - // console.log('KeyEntities', keyEntities); + + /** Get `missingRawValues` which not exist in the tree yet */ + const splitRawValues = React.useCallback( + (newRawValues: RawValueType[]) => { + const missingRawValues = []; + const existRawValues = []; + + // Keep missing value in the cache + newRawValues.forEach(val => { + if (valueEntities.has(val)) { + existRawValues.push(val); + } else { + missingRawValues.push(val); + } + }); + + return { missingRawValues, existRawValues }; + }, + [valueEntities], + ); // =========================== Label ============================ const getLabel = React.useCallback( @@ -330,24 +350,12 @@ const TreeSelect = React.forwardRef((props, ref) [mergedValues], ); - /** Get `missingRawValues` which not exist in the tree yet */ - const splitRawValues = React.useCallback( - (newRawValues: RawValueType[]) => { - const missingRawValues = []; - const existRawValues = []; - - // Keep missing value in the cache - newRawValues.forEach(val => { - if (valueEntities.has(val)) { - existRawValues.push(val); - } else { - missingRawValues.push(val); - } - }); - - return { missingRawValues, existRawValues }; - }, - [valueEntities], + // Convert value to key. Will fill missed keys for conduct check. + const [rawCheckedKeys, rawHalfCheckedKeys] = useCheckedKeys( + rawLabeledValues, + rawHalfCheckedValues, + treeConduction, + keyEntities, ); // =========================== Change =========================== @@ -499,8 +507,8 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - checkedKeys: rawValues, - // halfCheckedKeys: rawHalfCheckedKeys, + checkedKeys: rawCheckedKeys, + halfCheckedKeys: rawHalfCheckedKeys, // treeDefaultExpandAll, // treeExpandedKeys, // treeDefaultExpandedKeys, @@ -519,8 +527,8 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - rawValues, - // rawHalfCheckedKeys, + rawCheckedKeys, + rawHalfCheckedKeys, // treeDefaultExpandAll, // treeExpandedKeys, // treeDefaultExpandedKeys, diff --git a/src/hooks/useCheckedKeys.ts b/src/hooks/useCheckedKeys.ts new file mode 100644 index 00000000..59e38007 --- /dev/null +++ b/src/hooks/useCheckedKeys.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import type { DataEntity } from 'rc-tree/lib/interface'; +import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; +import type { LabeledValueType, RawValueType } from '../TreeSelect'; + +export default ( + rawLabeledValues: LabeledValueType[], + rawHalfCheckedValues: LabeledValueType[], + treeConduction: boolean, + keyEntities: Record, +) => + React.useMemo(() => { + let checkedKeys: RawValueType[] = rawLabeledValues.map(({ value }) => value); + let halfCheckedKeys: RawValueType[] = rawHalfCheckedValues.map(({ value }) => value); + + if (treeConduction) { + ({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities)); + } + + return [checkedKeys, halfCheckedKeys]; + }, [rawLabeledValues, rawHalfCheckedValues, treeConduction, keyEntities]); diff --git a/src/hooks/useDataEntities.ts b/src/hooks/useDataEntities.ts index fe37fec4..21e52edf 100644 --- a/src/hooks/useDataEntities.ts +++ b/src/hooks/useDataEntities.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; import type { DataEntity } from 'rc-tree/lib/interface'; -import type { FieldNames } from '../TreeSelect'; +import type { FieldNames, RawValueType } from '../TreeSelect'; export default (treeData: any, fieldNames: FieldNames) => React.useMemo<{ keyEntities: Record }>( @@ -17,4 +17,4 @@ export default (treeData: any, fieldNames: FieldNames) => }, }), [treeData, fieldNames], - ) as ReturnType & { valueEntities: Map }; + ) as ReturnType & { valueEntities: Map }; From 2d5692f9989e412b104cc92390640e296e506757 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 10 Dec 2021 18:09:20 +0800 Subject: [PATCH 08/37] chore: conduction tree de-select logic --- src/TreeSelect.tsx | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 5d1546aa..39d68c45 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -442,7 +442,7 @@ const TreeSelect = React.forwardRef((props, ref) // ========================== Options =========================== /** Trigger by option list */ const onOptionSelect: OnInternalSelect = React.useCallback( - (selectedKey, info) => { + (selectedKey, { selected }) => { const entity = keyEntities[selectedKey]; // const eventValue = mergedLabelInValue ? selectValue : selectValue; @@ -454,14 +454,31 @@ const TreeSelect = React.forwardRef((props, ref) // Single mode always set value triggerChange([selectedValue], { selected: true, triggerValue: selectedValue }, 'option'); } else { - let newRawValues = Array.from([...rawValues, selectedValue]); + let newRawValues = selected + ? [...rawValues, selectedValue] + : rawCheckedKeys.filter(v => v !== selectedValue); // Add keys if tree conduction if (treeConduction) { // Should keep missing values const { missingRawValues, existRawValues } = splitRawValues(newRawValues); const keyList = existRawValues.map(val => valueEntities.get(val).key); - const { checkedKeys } = conductCheck(keyList, true, keyEntities); + + // Conduction by selected or not + let checkedKeys: React.Key[]; + if (selected) { + ({ checkedKeys } = conductCheck(keyList, true, keyEntities)); + } else { + console.log('🎉', rawCheckedKeys, rawValues); + ({ checkedKeys } = conductCheck( + keyList, + { checked: false, halfCheckedKeys: rawHalfCheckedKeys }, + keyEntities, + )); + console.log('🧶', keyList, checkedKeys); + } + + // Fill back of keys newRawValues = [ ...missingRawValues, ...checkedKeys.map(key => keyEntities[key].node[mergedFieldNames.value]), From a53e69dbc224be6dd6ce52dc689aae87a65c2c8e Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 11:58:32 +0800 Subject: [PATCH 09/37] chore: back of filter --- examples/basic.tsx | 1 + examples/debug.tsx | 58 ++++++++++++++++++++++++++------ package.json | 2 +- src/TreeSelect.tsx | 48 +++++++++++++++++++++----- src/hooks/useFilterTreeData.ts | 61 ++++++++++++++++++++++++++++++++++ tests/Select.checkable.spec.js | 2 +- 6 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useFilterTreeData.ts diff --git a/examples/basic.tsx b/examples/basic.tsx index 8a84f565..69a52999 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -254,6 +254,7 @@ class Demo extends React.Component {

check select

{ - const [treeData, setTreeData] = React.useState([]); +const treeData = [ + { + title: 'Node1', + value: '0-0', + key: '0-0', + children: [ + { + title: 'Child Node1', + value: '0-0-0', + key: '0-0-0', + }, + ], + }, + { + title: 'Node2', + value: '0-1', + key: '0-1', + children: [ + { + title: 'Child Node3', + value: '0-1-0', + key: '0-1-0', + }, + { + title: 'Child Node4', + value: '0-1-1', + key: '0-1-1', + }, + { + title: 'Child Node5', + value: '0-1-2', + key: '0-1-2', + }, + ], + }, +]; - React.useEffect(() => { - setTimeout(() => { - console.clear(); - setTreeData([{ value: 'light', title: 'bamboo' }]); - }, 1000); - }, []); - - return ; -}; +export default () => ( + +); diff --git a/package.json b/package.json index 5f885cf4..9d7035a1 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.0.0-alpha.3", + "rc-select": "~14.0.0-alpha.4", "rc-tree": "~5.3.2", "rc-util": "^5.16.1" } diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 39d68c45..3a5106b8 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -31,10 +31,10 @@ import { } from './utils/valueUtil'; import useCache from './hooks/useCache'; import useRefFunc from './hooks/useRefFunc'; -import useChange from './hooks/useChange'; import useDataEntities from './hooks/useDataEntities'; import { fillAdditionalInfo } from './utils/legacyUtil'; import useCheckedKeys from './hooks/useCheckedKeys'; +import useFilterTreeData from './hooks/useFilterTreeData'; export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void; @@ -126,6 +126,8 @@ export interface TreeSelectProps void; autoClearSearchValue?: boolean; + filterTreeNode?: boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean); + treeNodeFilterProp?: string; // >>> Select onSelect?: SelectProps['onSelect']; @@ -149,6 +151,7 @@ export interface TreeSelectProps Promise; treeLoadedKeys?: React.Key[]; onTreeLoad?: (loadedKeys: React.Key[]) => void; + treeDefaultExpandAll?: boolean; // >>> Options virtual?: boolean; @@ -176,6 +179,8 @@ const TreeSelect = React.forwardRef((props, ref) inputValue, onSearch, autoClearSearchValue = true, + filterTreeNode, + treeNodeFilterProp = 'value', // Selector showCheckedStrategy = SHOW_CHILD, @@ -197,6 +202,7 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, + treeDefaultExpandAll, // Options virtual, @@ -257,6 +263,13 @@ const TreeSelect = React.forwardRef((props, ref) [valueEntities], ); + // Filtered Tree + const filteredTreeData = useFilterTreeData(mergedTreeData, mergedSearchValue, { + fieldNames: mergedFieldNames, + treeNodeFilterProp, + filterTreeNode, + }); + // =========================== Label ============================ const getLabel = React.useCallback( (item: DefaultOptionType) => { @@ -439,6 +452,23 @@ const TreeSelect = React.forwardRef((props, ref) }, ); + const onDisplayValuesChange = useRefFunc( + (newValues, info) => { + const newRawValues = newValues.map(item => item.value); + + const extraInfo = { + triggerValue: undefined, + selected: undefined, + }; + if (info.type !== 'clear') { + extraInfo.triggerValue = info.values[0].value; + extraInfo.selected = info.type === 'add'; + } + + triggerChange(newRawValues, extraInfo, 'selection'); + }, + ); + // ========================== Options =========================== /** Trigger by option list */ const onOptionSelect: OnInternalSelect = React.useCallback( @@ -469,13 +499,11 @@ const TreeSelect = React.forwardRef((props, ref) if (selected) { ({ checkedKeys } = conductCheck(keyList, true, keyEntities)); } else { - console.log('🎉', rawCheckedKeys, rawValues); ({ checkedKeys } = conductCheck( keyList, { checked: false, halfCheckedKeys: rawHalfCheckedKeys }, keyEntities, )); - console.log('🧶', keyList, checkedKeys); } // Fill back of keys @@ -501,6 +529,8 @@ const TreeSelect = React.forwardRef((props, ref) triggerChange, treeConduction, onSelect, + rawCheckedKeys, + rawHalfCheckedKeys, ], ); @@ -510,11 +540,11 @@ const TreeSelect = React.forwardRef((props, ref) virtual, listHeight, listItemHeight, - treeData: mergedTreeData, + treeData: filteredTreeData, fieldNames: mergedFieldNames, onSelect: onOptionSelect, }), - [virtual, listHeight, listItemHeight, mergedTreeData, mergedFieldNames, onOptionSelect], + [virtual, listHeight, listItemHeight, filteredTreeData, mergedFieldNames, onOptionSelect], ); // ======================= Legacy Context ======================= @@ -526,7 +556,7 @@ const TreeSelect = React.forwardRef((props, ref) onTreeLoad, checkedKeys: rawCheckedKeys, halfCheckedKeys: rawHalfCheckedKeys, - // treeDefaultExpandAll, + treeDefaultExpandAll, // treeExpandedKeys, // treeDefaultExpandedKeys, // onTreeExpand, @@ -546,7 +576,7 @@ const TreeSelect = React.forwardRef((props, ref) onTreeLoad, rawCheckedKeys, rawHalfCheckedKeys, - // treeDefaultExpandAll, + treeDefaultExpandAll, // treeExpandedKeys, // treeDefaultExpandedKeys, // onTreeExpand, @@ -571,8 +601,10 @@ const TreeSelect = React.forwardRef((props, ref) // >>> MISC id={mergedId} prefixCls={prefixCls} - displayValues={displayValues} mode={mergedMultiple ? 'multiple' : undefined} + // >>> Display Value + displayValues={displayValues} + onDisplayValuesChange={onDisplayValuesChange} // >>> Search searchValue={mergedSearchValue} onSearch={onInternalSearch} diff --git a/src/hooks/useFilterTreeData.ts b/src/hooks/useFilterTreeData.ts new file mode 100644 index 00000000..b97e0367 --- /dev/null +++ b/src/hooks/useFilterTreeData.ts @@ -0,0 +1,61 @@ +import * as React from 'react'; +import type { DefaultOptionType, InternalFieldName, TreeSelectProps } from '../TreeSelect'; +import { fillLegacyProps } from '../utils/legacyUtil'; + +type GetFuncType = T extends boolean ? never : T; +type FilterFn = GetFuncType; + +export default ( + treeData: DefaultOptionType[], + searchValue: string, + { + treeNodeFilterProp, + filterTreeNode, + fieldNames, + }: { + fieldNames: InternalFieldName; + treeNodeFilterProp: string; + filterTreeNode: TreeSelectProps['filterTreeNode']; + }, +) => { + const { children: fieldChildren } = fieldNames; + + return React.useMemo(() => { + if (!searchValue) { + return treeData; + } + + let filterOptionFunc: FilterFn; + if (typeof filterTreeNode === 'function') { + filterOptionFunc = filterTreeNode; + } else { + const upperStr = searchValue.toUpperCase(); + filterOptionFunc = (_, dataNode) => { + const value = dataNode[treeNodeFilterProp]; + + return String(value).toUpperCase().includes(upperStr); + }; + } + + function dig(list: DefaultOptionType[], keepAll: boolean = false) { + return list + .map(dataNode => { + const children = dataNode[fieldChildren]; + + const match = keepAll || filterOptionFunc(searchValue, fillLegacyProps(dataNode)); + const childList = dig(children || [], match); + + if (match || childList.length) { + return { + ...dataNode, + [fieldChildren]: childList, + }; + } + return null; + }) + .filter(node => node); + } + + return dig(treeData); + }, [treeData, searchValue, fieldChildren, treeNodeFilterProp, filterTreeNode]); +}; diff --git a/tests/Select.checkable.spec.js b/tests/Select.checkable.spec.js index 23e2a31b..d5d55e90 100644 --- a/tests/Select.checkable.spec.js +++ b/tests/Select.checkable.spec.js @@ -117,7 +117,7 @@ describe('TreeSelect.checkable', () => { onChange={this.handleChange} disabled={disabled} /> - this.switch(e.target.checked)} id="checkbox" />{' '} + this.switch(e.target.checked)} id="checkbox" /> 禁用 ); From f410c76991314b7d8b91699df0828021604012e1 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 14:39:29 +0800 Subject: [PATCH 10/37] fix: return type of onChange --- src/TreeSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 3a5106b8..e38f0e7f 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -439,7 +439,7 @@ const TreeSelect = React.forwardRef((props, ref) additionalInfo.selected = selected; } - const returnValues = returnLabeledValues + const returnValues = mergedLabelInValue ? returnLabeledValues : returnLabeledValues.map(item => item.value); From b813f7073d307d943cf4724c898bdb61fd3a499d Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 16:34:25 +0800 Subject: [PATCH 11/37] fix: filter hightlight logic --- examples/debug.tsx | 17 ++-- src/OptionList.tsx | 29 ++++--- src/TreeSelect.tsx | 85 +++++++++---------- .../Select.checkable.spec.js.snap | 2 - 4 files changed, 70 insertions(+), 63 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index 4c29e253..698ddb05 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -43,12 +43,17 @@ const treeData = [ export default () => ( + > + + + + + + ); diff --git a/src/OptionList.tsx b/src/OptionList.tsx index a1c4cdc1..427bcf11 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -9,7 +9,7 @@ import type { EventDataNode, ScrollTo } from 'rc-tree/lib/interface'; import type { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface'; import LegacyContext from './LegacyContext'; import TreeSelectContext from './TreeSelectContext'; -import { getAllKeys } from './utils/valueUtil'; +import { getAllKeys, isCheckDisabled } from './utils/valueUtil'; const HIDDEN_STYLE = { width: 0, @@ -58,8 +58,7 @@ const OptionList: React.RefForwardingComponent< ReviseRefOptionListProps, OptionListProps > = (props, ref) => { - const { prefixCls, multiple, searchValue, toggleOpen, open, notFoundContent, onMouseEnter } = - useBaseProps(); + const { prefixCls, multiple, searchValue, toggleOpen, open, notFoundContent } = useBaseProps(); const { virtual, listHeight, listItemHeight, treeData, fieldNames, onSelect } = React.useContext(TreeSelectContext); @@ -124,18 +123,20 @@ const OptionList: React.RefForwardingComponent< const [expandedKeys, setExpandedKeys] = React.useState(treeDefaultExpandedKeys); const [searchExpandedKeys, setSearchExpandedKeys] = React.useState(null); + const hasSearchValue = !!searchValue; + const mergedExpandedKeys = React.useMemo(() => { if (treeExpandedKeys) { return [...treeExpandedKeys]; } - return searchValue ? searchExpandedKeys : expandedKeys; - }, [expandedKeys, searchExpandedKeys, lowerSearchValue, treeExpandedKeys]); + return hasSearchValue ? searchExpandedKeys : expandedKeys; + }, [expandedKeys, searchExpandedKeys, treeExpandedKeys, hasSearchValue]); React.useEffect(() => { - if (searchValue) { + if (hasSearchValue) { setSearchExpandedKeys(getAllKeys(treeData, fieldNames)); } - }, [!!searchValue]); + }, [hasSearchValue]); const onInternalExpand = (keys: Key[]) => { setExpandedKeys(keys); @@ -152,8 +153,14 @@ const OptionList: React.RefForwardingComponent< }; const onInternalSelect = (_: Key[], info: TreeEventInfo) => { - onSelect(info.node.key, { - selected: !checkedKeys.includes(info.node.key), + const { node } = info; + + if (checkable && isCheckDisabled(node)) { + return; + } + + onSelect(node.key, { + selected: !checkedKeys.includes(node.key), }); if (!multiple) { @@ -221,7 +228,7 @@ const OptionList: React.RefForwardingComponent< } return ( -
+
{activeEntity && open && ( {activeEntity.data.value} @@ -247,7 +254,7 @@ const OptionList: React.RefForwardingComponent< checkable={checkable} checkStrictly checkedKeys={mergedCheckedKeys} - selectedKeys={!checkable ? valueKeys : []} + selectedKeys={!checkable ? checkedKeys : []} defaultExpandAll={treeDefaultExpandAll} {...treeProps} // Proxy event out diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index e38f0e7f..ddfd5c46 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -18,17 +18,7 @@ import TreeSelectContext from './TreeSelectContext'; import type { TreeSelectContextProps } from './TreeSelectContext'; import LegacyContext from './LegacyContext'; import useTreeData from './hooks/useTreeData'; -import { - flattenOptions, - filterOptions, - isValueDisabled, - findValueOption, - addValue, - removeValue, - getRawValueLabeled, - toArray, - fillFieldNames, -} from './utils/valueUtil'; +import { toArray, fillFieldNames } from './utils/valueUtil'; import useCache from './hooks/useCache'; import useRefFunc from './hooks/useRefFunc'; import useDataEntities from './hooks/useDataEntities'; @@ -293,46 +283,56 @@ const TreeSelect = React.forwardRef((props, ref) ); // ========================= Wrap Value ========================= + const toLabeledValues = React.useCallback((draftValues: ValueType) => { + const values = toArray(draftValues); + + return values.map(val => { + if (isRawValue(val)) { + return { value: val }; + } + return val; + }); + }, []); + const convert2LabelValues = React.useCallback( (draftValues: ValueType) => { - const values = toArray(draftValues); - - return values.map(val => { - let rawLabel: React.ReactNode; - let rawValue: RawValueType; - let rawHalfChecked: boolean; - - // Init provided info - if (!isRawValue(val)) { - rawLabel = val.label; - rawValue = val.value; - rawHalfChecked = val.halfChecked; - } else { - rawValue = val; - } + const values = toLabeledValues(draftValues); + + return values.map(item => { + let { label: rawLabel } = item; + const { value: rawValue, halfChecked: rawHalfChecked } = item; + + let rawDisabled: boolean | undefined; - // Fill missing label - if (rawLabel === undefined) { - const entity = valueEntities.get(rawValue); - rawLabel = getLabel(entity?.node); + const entity = valueEntities.get(rawValue); + + // Fill missing label & status + if (entity) { + rawLabel = rawLabel ?? getLabel(entity.node); + rawDisabled = entity.node.disabled; } return { label: rawLabel, value: rawValue, halfChecked: rawHalfChecked, + disabled: rawDisabled, }; }); }, - [valueEntities, getLabel], + [valueEntities, getLabel, toLabeledValues], ); // =========================== Values =========================== const [internalValue, setInternalValue] = useMergedState(defaultValue, { value }); + // const rawMixedLabeledValues = React.useMemo( + // () => convert2LabelValues(internalValue), + // [convert2LabelValues, internalValue], + // ); const rawMixedLabeledValues = React.useMemo( - () => convert2LabelValues(internalValue), - [convert2LabelValues, internalValue], + () => toLabeledValues(internalValue), + [toLabeledValues, internalValue], ); // Split value into full check and half check @@ -354,15 +354,6 @@ const TreeSelect = React.forwardRef((props, ref) const [mergedValues] = useCache(rawLabeledValues); const rawValues = React.useMemo(() => mergedValues.map(item => item.value), [mergedValues]); - const displayValues = React.useMemo( - () => - mergedValues.map(item => ({ - ...item, - label: item.label ?? item.value, - })), - [mergedValues], - ); - // Convert value to key. Will fill missed keys for conduct check. const [rawCheckedKeys, rawHalfCheckedKeys] = useCheckedKeys( rawLabeledValues, @@ -371,6 +362,12 @@ const TreeSelect = React.forwardRef((props, ref) keyEntities, ); + // Convert rawCheckedKeys to check strategy related values + const displayValues = React.useMemo( + () => convert2LabelValues(rawCheckedKeys), + [rawCheckedKeys, convert2LabelValues], + ); + // =========================== Change =========================== const triggerChange = useRefFunc( ( @@ -565,7 +562,7 @@ const TreeSelect = React.forwardRef((props, ref) // showTreeIcon, // switcherIcon, // treeLine, - // treeNodeFilterProp, + treeNodeFilterProp, // getEntityByKey, // getEntityByValue, }), @@ -585,7 +582,7 @@ const TreeSelect = React.forwardRef((props, ref) // showTreeIcon, // switcherIcon, // treeLine, - // treeNodeFilterProp, + treeNodeFilterProp, // getEntityByKey, // getEntityByValue, ], diff --git a/tests/__snapshots__/Select.checkable.spec.js.snap b/tests/__snapshots__/Select.checkable.spec.js.snap index 043055b2..c96fd682 100644 --- a/tests/__snapshots__/Select.checkable.spec.js.snap +++ b/tests/__snapshots__/Select.checkable.spec.js.snap @@ -573,7 +573,6 @@ exports[`TreeSelect.checkable uncheck remove by tree check 1`] = ` style="width: 0px;" > Date: Mon, 13 Dec 2021 17:02:40 +0800 Subject: [PATCH 12/37] fix: display logic --- examples/debug.tsx | 50 ++++++++++++++----------------------------- src/TreeSelect.tsx | 41 +++++++++++++++++++++++++---------- src/hooks/useCache.ts | 3 +-- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index 698ddb05..6bc16088 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -6,36 +6,28 @@ import '../assets/index.less'; const treeData = [ { - title: 'Node1', - value: '0-0', - key: '0-0', + key: 'P001', + title: 'P001', + value: 'P001', children: [ { - title: 'Child Node1', - value: '0-0-0', - key: '0-0-0', + key: '0020', + title: '0020', + value: '0020', + children: [{ key: '9459', title: '9459', value: '9459' }], }, ], }, { - title: 'Node2', - value: '0-1', - key: '0-1', + key: 'P002', + title: 'P002', + value: 'P002', children: [ { - title: 'Child Node3', - value: '0-1-0', - key: '0-1-0', - }, - { - title: 'Child Node4', - value: '0-1-1', - key: '0-1-1', - }, - { - title: 'Child Node5', - value: '0-1-2', - key: '0-1-2', + key: '0021', + title: '0021', + value: '0021', + children: [{ key: '9458', title: '9458', value: '9458' }], }, ], }, @@ -43,17 +35,7 @@ const treeData = [ export default () => ( - - - - - - + treeCheckable treeData={treeData} open + /> ); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index ddfd5c46..5a5cfa20 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -326,10 +326,6 @@ const TreeSelect = React.forwardRef((props, ref) // =========================== Values =========================== const [internalValue, setInternalValue] = useMergedState(defaultValue, { value }); - // const rawMixedLabeledValues = React.useMemo( - // () => convert2LabelValues(internalValue), - // [convert2LabelValues, internalValue], - // ); const rawMixedLabeledValues = React.useMemo( () => toLabeledValues(internalValue), [toLabeledValues, internalValue], @@ -351,8 +347,11 @@ const TreeSelect = React.forwardRef((props, ref) return [fullCheckValues, halfCheckValues]; }, [rawMixedLabeledValues]); - const [mergedValues] = useCache(rawLabeledValues); - const rawValues = React.useMemo(() => mergedValues.map(item => item.value), [mergedValues]); + // const [mergedValues] = useCache(rawLabeledValues); + const rawValues = React.useMemo( + () => rawLabeledValues.map(item => item.value), + [rawLabeledValues], + ); // Convert value to key. Will fill missed keys for conduct check. const [rawCheckedKeys, rawHalfCheckedKeys] = useCheckedKeys( @@ -363,10 +362,25 @@ const TreeSelect = React.forwardRef((props, ref) ); // Convert rawCheckedKeys to check strategy related values - const displayValues = React.useMemo( - () => convert2LabelValues(rawCheckedKeys), - [rawCheckedKeys, convert2LabelValues], - ); + const displayValues = React.useMemo(() => { + // Collect keys which need to show + const displayKeys = + showCheckedStrategy === 'SHOW_ALL' + ? rawCheckedKeys + : formatStrategyKeys(rawCheckedKeys, showCheckedStrategy, keyEntities); + + // Convert to value and filled with label + const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); + return convert2LabelValues(values); + }, [ + rawCheckedKeys, + convert2LabelValues, + showCheckedStrategy, + keyEntities, + mergedFieldNames.value, + ]); + + const [cachedDisplayValues] = useCache(displayValues); // =========================== Change =========================== const triggerChange = useRefFunc( @@ -377,6 +391,11 @@ const TreeSelect = React.forwardRef((props, ref) ) => { const labeledValues = convert2LabelValues(newRawValues); setInternalValue(labeledValues); + + // Clean up if needed + if (autoClearSearchValue) { + setSearchValue(''); + } // Generate rest parameters is costly, so only do it when necessary if (onChange) { @@ -600,7 +619,7 @@ const TreeSelect = React.forwardRef((props, ref) prefixCls={prefixCls} mode={mergedMultiple ? 'multiple' : undefined} // >>> Display Value - displayValues={displayValues} + displayValues={cachedDisplayValues} onDisplayValuesChange={onDisplayValuesChange} // >>> Search searchValue={mergedSearchValue} diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts index 06b1defb..db939e5f 100644 --- a/src/hooks/useCache.ts +++ b/src/hooks/useCache.ts @@ -1,6 +1,5 @@ import * as React from 'react'; -import type { DataEntity } from 'rc-tree/lib/interface'; -import type { DefaultOptionType, LabeledValueType, RawValueType } from '../TreeSelect'; +import type { LabeledValueType, RawValueType } from '../TreeSelect'; /** * This function will try to call requestIdleCallback if available to save performance. From e7a3087e539b0b92aeb3afcf7597f87ef2637bf1 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 17:11:21 +0800 Subject: [PATCH 13/37] fix: onChange return ueseless content --- src/TreeSelect.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 5a5cfa20..bc443ec2 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -391,7 +391,7 @@ const TreeSelect = React.forwardRef((props, ref) ) => { const labeledValues = convert2LabelValues(newRawValues); setInternalValue(labeledValues); - + // Clean up if needed if (autoClearSearchValue) { setSearchValue(''); @@ -417,15 +417,16 @@ const TreeSelect = React.forwardRef((props, ref) selected: undefined, }; - let returnLabeledValues: LabeledValueType[] = convert2LabelValues(eventValues); + let returnRawValues: (LabeledValueType | RawValueType)[] = eventValues; // We need fill half check back if (treeCheckStrictly) { const halfValues = rawHalfCheckedValues.filter(item => !eventValues.includes(item.value)); - returnLabeledValues = [...returnLabeledValues, ...halfValues]; + returnRawValues = [...returnRawValues, ...halfValues]; } + const returnLabeledValues = convert2LabelValues(returnRawValues); const additionalInfo = { // [Legacy] Always return as array contains label & value preValue: rawLabeledValues, From ccb220337e643986feebeaed7ddba53b87c94b9b Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 17:34:56 +0800 Subject: [PATCH 14/37] fix: Selection remove also need conduction --- examples/debug.tsx | 15 +++++++- src/TreeSelect.tsx | 38 +++++++++---------- .../Select.checkable.spec.js.snap | 2 - 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index 6bc16088..c9f727da 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -4,6 +4,8 @@ import React from 'react'; import TreeSelect from '../src'; import '../assets/index.less'; +const { TreeNode, SHOW_ALL } = TreeSelect; + const treeData = [ { key: 'P001', @@ -36,6 +38,15 @@ const treeData = [ export default () => ( + showCheckedStrategy={SHOW_ALL} + treeCheckable + defaultValue={['0']} + open + > + + + + + + ); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index bc443ec2..a101d5d4 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -386,7 +386,7 @@ const TreeSelect = React.forwardRef((props, ref) const triggerChange = useRefFunc( ( newRawValues: RawValueType[], - extra: { triggerValue: RawValueType; selected: boolean }, + extra: { triggerValue?: RawValueType; selected?: boolean }, source: SelectSource, ) => { const labeledValues = convert2LabelValues(newRawValues); @@ -447,7 +447,7 @@ const TreeSelect = React.forwardRef((props, ref) newRawValues, mergedTreeData, showPosition, - fieldNames, + mergedFieldNames, ); if (mergedCheckable) { @@ -469,23 +469,6 @@ const TreeSelect = React.forwardRef((props, ref) }, ); - const onDisplayValuesChange = useRefFunc( - (newValues, info) => { - const newRawValues = newValues.map(item => item.value); - - const extraInfo = { - triggerValue: undefined, - selected: undefined, - }; - if (info.type !== 'clear') { - extraInfo.triggerValue = info.values[0].value; - extraInfo.selected = info.type === 'add'; - } - - triggerChange(newRawValues, extraInfo, 'selection'); - }, - ); - // ========================== Options =========================== /** Trigger by option list */ const onOptionSelect: OnInternalSelect = React.useCallback( @@ -551,6 +534,23 @@ const TreeSelect = React.forwardRef((props, ref) ], ); + // ====================== Display Change ======================== + const onDisplayValuesChange = useRefFunc( + (newValues, info) => { + const newRawValues = newValues.map(item => item.value); + + if (info.type === 'clear') { + triggerChange(newRawValues, {}, 'selection'); + return; + } + + // TreeSelect only have multiple mode which means display change only has remove + if (info.values.length) { + onOptionSelect(info.values[0].value, { selected: false }); + } + }, + ); + // ========================== Context =========================== const treeSelectContext = React.useMemo( () => ({ diff --git a/tests/__snapshots__/Select.checkable.spec.js.snap b/tests/__snapshots__/Select.checkable.spec.js.snap index c96fd682..80018305 100644 --- a/tests/__snapshots__/Select.checkable.spec.js.snap +++ b/tests/__snapshots__/Select.checkable.spec.js.snap @@ -101,7 +101,6 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 1 style="width: 0px;" > Date: Mon, 13 Dec 2021 17:47:11 +0800 Subject: [PATCH 15/37] fix: Missing expandedKeys --- src/TreeSelect.tsx | 6 ++++-- tests/Select.SearchInput.spec.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index a101d5d4..874f1956 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -142,6 +142,7 @@ export interface TreeSelectProps void; treeDefaultExpandAll?: boolean; + treeExpandedKeys?: React.Key[]; // >>> Options virtual?: boolean; @@ -193,6 +194,7 @@ const TreeSelect = React.forwardRef((props, ref) treeLoadedKeys, onTreeLoad, treeDefaultExpandAll, + treeExpandedKeys, // Options virtual, @@ -574,7 +576,7 @@ const TreeSelect = React.forwardRef((props, ref) checkedKeys: rawCheckedKeys, halfCheckedKeys: rawHalfCheckedKeys, treeDefaultExpandAll, - // treeExpandedKeys, + treeExpandedKeys, // treeDefaultExpandedKeys, // onTreeExpand, // treeIcon, @@ -594,7 +596,7 @@ const TreeSelect = React.forwardRef((props, ref) rawCheckedKeys, rawHalfCheckedKeys, treeDefaultExpandAll, - // treeExpandedKeys, + treeExpandedKeys, // treeDefaultExpandedKeys, // onTreeExpand, // treeIcon, diff --git a/tests/Select.SearchInput.spec.js b/tests/Select.SearchInput.spec.js index 4127f184..3b6861c3 100644 --- a/tests/Select.SearchInput.spec.js +++ b/tests/Select.SearchInput.spec.js @@ -48,7 +48,7 @@ describe('TreeSelect.SearchInput', () => { />, ); - expect(wrapper.find('NodeList').props().expandedKeys).toEqual(['bamboo', 'light']); + expect(wrapper.find('NodeList').prop('expandedKeys')).toEqual(['bamboo', 'light']); function search(value) { wrapper From 68d0a5004703712d532784d744b7c8b6c82ab852 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 18:04:55 +0800 Subject: [PATCH 16/37] fix: wrap with props --- src/TreeSelect.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 874f1956..5ffeaad0 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -22,7 +22,7 @@ import { toArray, fillFieldNames } from './utils/valueUtil'; import useCache from './hooks/useCache'; import useRefFunc from './hooks/useRefFunc'; import useDataEntities from './hooks/useDataEntities'; -import { fillAdditionalInfo } from './utils/legacyUtil'; +import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil'; import useCheckedKeys from './hooks/useCheckedKeys'; import useFilterTreeData from './hooks/useFilterTreeData'; @@ -121,6 +121,7 @@ export interface TreeSelectProps>> Select onSelect?: SelectProps['onSelect']; + onDeselect?: SelectProps['onDeselect']; // >>> Selector showCheckedStrategy?: CheckedStrategy; @@ -164,6 +165,7 @@ const TreeSelect = React.forwardRef((props, ref) defaultValue, onChange, onSelect, + onDeselect, // Search searchValue, @@ -518,7 +520,11 @@ const TreeSelect = React.forwardRef((props, ref) } // Trigger select event - onSelect?.(selectedValue, entity.node); + if (selected) { + onSelect?.(selectedValue, fillLegacyProps(entity.node)); + } else { + onDeselect?.(selectedValue, fillLegacyProps(entity.node)); + } } }, [ @@ -531,6 +537,7 @@ const TreeSelect = React.forwardRef((props, ref) triggerChange, treeConduction, onSelect, + onDeselect, rawCheckedKeys, rawHalfCheckedKeys, ], From e80ca74902ed5951f967d5e94aa6ae49a9182d44 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 13 Dec 2021 19:37:01 +0800 Subject: [PATCH 17/37] fix: empty value should show placeholder --- src/TreeSelect.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 5ffeaad0..6d917259 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -155,6 +155,10 @@ function isRawValue(value: RawValueType | LabeledValueType): value is RawValueTy return !value || typeof value !== 'object'; } +function isNil(val: any) { + return val === null || val === undefined; +} + const TreeSelect = React.forwardRef((props, ref) => { const { id, @@ -375,7 +379,15 @@ const TreeSelect = React.forwardRef((props, ref) // Convert to value and filled with label const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); - return convert2LabelValues(values); + const rawDisplayValues = convert2LabelValues(values); + + const firstVal = rawDisplayValues[0]; + + if (rawDisplayValues.length === 1 && isNil(firstVal.value) && isNil(firstVal.label)) { + return []; + } + + return rawDisplayValues; }, [ rawCheckedKeys, convert2LabelValues, From d5d2d909093c2da9d309203482e9665c5ad38df8 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:09:07 +0800 Subject: [PATCH 18/37] fix: single null --- src/TreeSelect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 6d917259..6e6f7b55 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -383,12 +383,13 @@ const TreeSelect = React.forwardRef((props, ref) const firstVal = rawDisplayValues[0]; - if (rawDisplayValues.length === 1 && isNil(firstVal.value) && isNil(firstVal.label)) { + if (!mergedMultiple && isNil(firstVal.value) && isNil(firstVal.label)) { return []; } return rawDisplayValues; }, [ + mergedMultiple, rawCheckedKeys, convert2LabelValues, showCheckedStrategy, From 707f5feb07fbbbbfc26985095cd0d2364087a385 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:19:21 +0800 Subject: [PATCH 19/37] fix: check logic conduction --- examples/debug.tsx | 26 +++++++++++++++++--------- src/TreeSelect.tsx | 17 +++++++++-------- src/generate.tsx | 4 ++-- src/hooks/useSelectValues.ts | 4 ++-- src/utils/strategyUtil.ts | 22 ++++++++++++---------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index c9f727da..100d826f 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -4,7 +4,7 @@ import React from 'react'; import TreeSelect from '../src'; import '../assets/index.less'; -const { TreeNode, SHOW_ALL } = TreeSelect; +const { TreeNode, SHOW_ALL, SHOW_CHILD } = TreeSelect; const treeData = [ { @@ -38,15 +38,23 @@ const treeData = [ export default () => ( - - - - - - + /> ); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 6e6f7b55..66a9dc14 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -12,7 +12,7 @@ import useId from 'rc-select/lib/hooks/useId'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import OptionList from './OptionList'; import TreeNode from './TreeNode'; -import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; +import { formatStrategyValues, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; import type { CheckedStrategy } from './utils/strategyUtil'; import TreeSelectContext from './TreeSelectContext'; import type { TreeSelectContextProps } from './TreeSelectContext'; @@ -375,7 +375,7 @@ const TreeSelect = React.forwardRef((props, ref) const displayKeys = showCheckedStrategy === 'SHOW_ALL' ? rawCheckedKeys - : formatStrategyKeys(rawCheckedKeys, showCheckedStrategy, keyEntities); + : formatStrategyValues(rawCheckedKeys, showCheckedStrategy, keyEntities, mergedFieldNames); // Convert to value and filled with label const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); @@ -389,12 +389,12 @@ const TreeSelect = React.forwardRef((props, ref) return rawDisplayValues; }, [ + mergedFieldNames, mergedMultiple, rawCheckedKeys, convert2LabelValues, showCheckedStrategy, keyEntities, - mergedFieldNames.value, ]); const [cachedDisplayValues] = useCache(displayValues); @@ -418,11 +418,12 @@ const TreeSelect = React.forwardRef((props, ref) if (onChange) { let eventValues: RawValueType[] = newRawValues; if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') { - const keyList = newRawValues.map(val => { - const entity = valueEntities.get(val); - return entity?.key ?? val; - }); - const formattedKeyList = formatStrategyKeys(keyList, showCheckedStrategy, keyEntities); + const formattedKeyList = formatStrategyValues( + newRawValues, + showCheckedStrategy, + keyEntities, + mergedFieldNames, + ); eventValues = formattedKeyList.map(key => { const entity = valueEntities.get(key); return entity ? entity.node[mergedFieldNames.value] : key; diff --git a/src/generate.tsx b/src/generate.tsx index 2047516d..53d93c2b 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -42,7 +42,7 @@ import useTreeData from './hooks/useTreeData'; import useKeyValueMap from './hooks/useKeyValueMap'; import useKeyValueMapping from './hooks/useKeyValueMapping'; import type { CheckedStrategy } from './utils/strategyUtil'; -import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; +import { formatStrategyValues, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; import { fillAdditionalInfo } from './utils/legacyUtil'; import useSelectValues from './hooks/useSelectValues'; @@ -357,7 +357,7 @@ export default function generate(config: { const entity = getEntityByValue(val); return entity ? entity.key : val; }); - const formattedKeyList = formatStrategyKeys( + const formattedKeyList = formatStrategyValues( keyList, showCheckedStrategy, conductKeyEntities, diff --git a/src/hooks/useSelectValues.ts b/src/hooks/useSelectValues.ts index b2e55003..4cffd081 100644 --- a/src/hooks/useSelectValues.ts +++ b/src/hooks/useSelectValues.ts @@ -5,7 +5,7 @@ import type { RawValueType, FlattenDataNode, Key, LabelValueType } from '../inte import type { SkipType } from './useKeyValueMapping'; import { getRawValueLabeled } from '../utils/valueUtil'; import type { CheckedStrategy } from '../utils/strategyUtil'; -import { formatStrategyKeys } from '../utils/strategyUtil'; +import { formatStrategyValues } from '../utils/strategyUtil'; interface Config { treeConduction: boolean; @@ -39,7 +39,7 @@ export default function useSelectValues( let mergedRawValues = rawValues; if (treeConduction) { - const rawKeys = formatStrategyKeys( + const rawKeys = formatStrategyValues( rawValues.map(val => { const entity = getEntityByValue(val); return entity ? entity.key : val; diff --git a/src/utils/strategyUtil.ts b/src/utils/strategyUtil.ts index 02bd1896..4eacece8 100644 --- a/src/utils/strategyUtil.ts +++ b/src/utils/strategyUtil.ts @@ -1,6 +1,7 @@ -import type { DataEntity } from 'rc-tree/lib/interface'; import type * as React from 'react'; -import type { RawValueType, Key, DataNode } from '../interface'; +import type { InternalFieldName } from '../TreeSelect'; +import type { DataEntity } from 'rc-tree/lib/interface'; +import type { RawValueType, Key } from '../interface'; import { isCheckDisabled } from './valueUtil'; export const SHOW_ALL = 'SHOW_ALL'; @@ -9,22 +10,23 @@ export const SHOW_CHILD = 'SHOW_CHILD'; export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD; -export function formatStrategyKeys( - keys: React.Key[], +export function formatStrategyValues( + values: React.Key[], strategy: CheckedStrategy, keyEntities: Record, + fieldNames: InternalFieldName, ): RawValueType[] { - const keySet = new Set(keys); + const valueSet = new Set(values); if (strategy === SHOW_CHILD) { - return keys.filter(key => { + return values.filter(key => { const entity = keyEntities[key]; if ( entity && entity.children && entity.children.every( - ({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key), + ({ node }) => isCheckDisabled(node) || valueSet.has(node[fieldNames.value]), ) ) { return false; @@ -33,15 +35,15 @@ export function formatStrategyKeys( }); } if (strategy === SHOW_PARENT) { - return keys.filter(key => { + return values.filter(key => { const entity = keyEntities[key]; const parent = entity ? entity.parent : null; - if (parent && !isCheckDisabled(parent.node) && keySet.has(parent.key)) { + if (parent && !isCheckDisabled(parent.node) && valueSet.has(parent.key)) { return false; } return true; }); } - return keys; + return values; } From dee1c919a04e967d877dba4ebe294d0d5c57dc29 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:21:04 +0800 Subject: [PATCH 20/37] chore: rename --- src/TreeSelect.tsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 66a9dc14..23f9c7fb 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -340,7 +340,7 @@ const TreeSelect = React.forwardRef((props, ref) ); // Split value into full check and half check - const [rawLabeledValues, rawHalfCheckedValues] = React.useMemo(() => { + const [rawLabeledValues, rawHalfLabeledValues] = React.useMemo(() => { const fullCheckValues: LabeledValueType[] = []; const halfCheckValues: LabeledValueType[] = []; @@ -362,9 +362,9 @@ const TreeSelect = React.forwardRef((props, ref) ); // Convert value to key. Will fill missed keys for conduct check. - const [rawCheckedKeys, rawHalfCheckedKeys] = useCheckedKeys( + const [rawCheckedValues, rawHalfCheckedValues] = useCheckedKeys( rawLabeledValues, - rawHalfCheckedValues, + rawHalfLabeledValues, treeConduction, keyEntities, ); @@ -374,8 +374,8 @@ const TreeSelect = React.forwardRef((props, ref) // Collect keys which need to show const displayKeys = showCheckedStrategy === 'SHOW_ALL' - ? rawCheckedKeys - : formatStrategyValues(rawCheckedKeys, showCheckedStrategy, keyEntities, mergedFieldNames); + ? rawCheckedValues + : formatStrategyValues(rawCheckedValues, showCheckedStrategy, keyEntities, mergedFieldNames); // Convert to value and filled with label const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); @@ -391,7 +391,7 @@ const TreeSelect = React.forwardRef((props, ref) }, [ mergedFieldNames, mergedMultiple, - rawCheckedKeys, + rawCheckedValues, convert2LabelValues, showCheckedStrategy, keyEntities, @@ -439,7 +439,7 @@ const TreeSelect = React.forwardRef((props, ref) // We need fill half check back if (treeCheckStrictly) { - const halfValues = rawHalfCheckedValues.filter(item => !eventValues.includes(item.value)); + const halfValues = rawHalfLabeledValues.filter(item => !eventValues.includes(item.value)); returnRawValues = [...returnRawValues, ...halfValues]; } @@ -504,7 +504,7 @@ const TreeSelect = React.forwardRef((props, ref) } else { let newRawValues = selected ? [...rawValues, selectedValue] - : rawCheckedKeys.filter(v => v !== selectedValue); + : rawCheckedValues.filter(v => v !== selectedValue); // Add keys if tree conduction if (treeConduction) { @@ -519,7 +519,7 @@ const TreeSelect = React.forwardRef((props, ref) } else { ({ checkedKeys } = conductCheck( keyList, - { checked: false, halfCheckedKeys: rawHalfCheckedKeys }, + { checked: false, halfCheckedKeys: rawHalfCheckedValues }, keyEntities, )); } @@ -552,8 +552,8 @@ const TreeSelect = React.forwardRef((props, ref) treeConduction, onSelect, onDeselect, - rawCheckedKeys, - rawHalfCheckedKeys, + rawCheckedValues, + rawHalfCheckedValues, ], ); @@ -594,8 +594,8 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - checkedKeys: rawCheckedKeys, - halfCheckedKeys: rawHalfCheckedKeys, + checkedKeys: rawCheckedValues, + halfCheckedKeys: rawHalfCheckedValues, treeDefaultExpandAll, treeExpandedKeys, // treeDefaultExpandedKeys, @@ -614,8 +614,8 @@ const TreeSelect = React.forwardRef((props, ref) loadData, treeLoadedKeys, onTreeLoad, - rawCheckedKeys, - rawHalfCheckedKeys, + rawCheckedValues, + rawHalfCheckedValues, treeDefaultExpandAll, treeExpandedKeys, // treeDefaultExpandedKeys, From 6b00058f0ff0942a66c3267161d88e90dfcd7d15 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:25:01 +0800 Subject: [PATCH 21/37] fix: first value logic --- src/TreeSelect.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 23f9c7fb..e1a3336f 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -375,7 +375,12 @@ const TreeSelect = React.forwardRef((props, ref) const displayKeys = showCheckedStrategy === 'SHOW_ALL' ? rawCheckedValues - : formatStrategyValues(rawCheckedValues, showCheckedStrategy, keyEntities, mergedFieldNames); + : formatStrategyValues( + rawCheckedValues, + showCheckedStrategy, + keyEntities, + mergedFieldNames, + ); // Convert to value and filled with label const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); @@ -383,7 +388,7 @@ const TreeSelect = React.forwardRef((props, ref) const firstVal = rawDisplayValues[0]; - if (!mergedMultiple && isNil(firstVal.value) && isNil(firstVal.label)) { + if (!mergedMultiple && firstVal && isNil(firstVal.value) && isNil(firstVal.label)) { return []; } From cc4c3adbaf1de76a27c625bc28be27fd4f4ab60a Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:41:08 +0800 Subject: [PATCH 22/37] fix: search logic --- examples/debug.tsx | 35 +++++++++++++++++----------------- src/OptionList.tsx | 10 ++++------ src/hooks/useFilterTreeData.ts | 2 +- tests/Select.props.spec.js | 2 +- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index 100d826f..c0aea04c 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -5,6 +5,7 @@ import TreeSelect from '../src'; import '../assets/index.less'; const { TreeNode, SHOW_ALL, SHOW_CHILD } = TreeSelect; +const SelectNode = TreeNode; const treeData = [ { @@ -35,26 +36,24 @@ const treeData = [ }, ]; +function filterTreeNode(input, child) { + return String(child.props.title).indexOf(input) !== -1; +} + export default () => ( + > + + + + + + ); diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 427bcf11..e394bf8d 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -123,20 +123,18 @@ const OptionList: React.RefForwardingComponent< const [expandedKeys, setExpandedKeys] = React.useState(treeDefaultExpandedKeys); const [searchExpandedKeys, setSearchExpandedKeys] = React.useState(null); - const hasSearchValue = !!searchValue; - const mergedExpandedKeys = React.useMemo(() => { if (treeExpandedKeys) { return [...treeExpandedKeys]; } - return hasSearchValue ? searchExpandedKeys : expandedKeys; - }, [expandedKeys, searchExpandedKeys, treeExpandedKeys, hasSearchValue]); + return searchValue ? searchExpandedKeys : expandedKeys; + }, [expandedKeys, searchExpandedKeys, treeExpandedKeys, searchValue]); React.useEffect(() => { - if (hasSearchValue) { + if (searchValue) { setSearchExpandedKeys(getAllKeys(treeData, fieldNames)); } - }, [hasSearchValue]); + }, [searchValue]); const onInternalExpand = (keys: Key[]) => { setExpandedKeys(keys); diff --git a/src/hooks/useFilterTreeData.ts b/src/hooks/useFilterTreeData.ts index b97e0367..016c573a 100644 --- a/src/hooks/useFilterTreeData.ts +++ b/src/hooks/useFilterTreeData.ts @@ -21,7 +21,7 @@ export default ( const { children: fieldChildren } = fieldNames; return React.useMemo(() => { - if (!searchValue) { + if (!searchValue || filterTreeNode === false) { return treeData; } diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js index 724cfc14..e64c49b5 100644 --- a/tests/Select.props.spec.js +++ b/tests/Select.props.spec.js @@ -38,7 +38,7 @@ describe('TreeSelect.props', () => { }); describe('filterTreeNode', () => { - it('function', () => { + it('as function', () => { function filterTreeNode(input, child) { return String(child.props.title).indexOf(input) !== -1; } From 5f3b9cb55f5cd8b75c992e83e943326ca80a0f07 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:48:54 +0800 Subject: [PATCH 23/37] fix: single always trigger onSelect --- examples/debug.tsx | 4 ++-- src/TreeSelect.tsx | 9 ++++++--- tests/Select.props.spec.js | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index c0aea04c..f7167d87 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -43,8 +43,8 @@ function filterTreeNode(input, child) { export default () => ( ((props, ref) ); // Convert to value and filled with label - const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value]); + const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value] ?? key); const rawDisplayValues = convert2LabelValues(values); const firstVal = rawDisplayValues[0]; @@ -392,7 +392,10 @@ const TreeSelect = React.forwardRef((props, ref) return []; } - return rawDisplayValues; + return rawDisplayValues.map(item => ({ + ...item, + label: item.label ?? item.value, + })); }, [ mergedFieldNames, mergedMultiple, @@ -539,7 +542,7 @@ const TreeSelect = React.forwardRef((props, ref) } // Trigger select event - if (selected) { + if (selected || !mergedMultiple) { onSelect?.(selectedValue, fillLegacyProps(entity.node)); } else { onDeselect?.(selectedValue, fillLegacyProps(entity.node)); diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js index e64c49b5..6ba15fd8 100644 --- a/tests/Select.props.spec.js +++ b/tests/Select.props.spec.js @@ -488,7 +488,7 @@ describe('TreeSelect.props', () => { }); describe('single', () => { - it('click on tree', () => { + it('click on tree node', () => { const onSelect = jest.fn(); const onDeselect = jest.fn(); const wrapper = createDeselectWrapper({ From 1a66d92132f03dd8cbecad0ca48857206c2e3a18 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 14 Dec 2021 11:58:58 +0800 Subject: [PATCH 24/37] fix: update snapshot --- examples/debug.tsx | 20 ++++++-------------- src/TreeSelect.tsx | 6 ++++-- tests/Select.spec.js | 1 - tests/__snapshots__/Select.spec.js.snap | 2 -- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index f7167d87..3b1c7874 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -41,19 +41,11 @@ function filterTreeNode(input, child) { } export default () => ( - - - - - - + + + + + + ); diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 4d30f0bb..4d29d5b0 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -144,6 +144,7 @@ export interface TreeSelectProps void; treeDefaultExpandAll?: boolean; treeExpandedKeys?: React.Key[]; + treeDefaultExpandedKeys?: React.Key[]; // >>> Options virtual?: boolean; @@ -201,6 +202,7 @@ const TreeSelect = React.forwardRef((props, ref) onTreeLoad, treeDefaultExpandAll, treeExpandedKeys, + treeDefaultExpandedKeys, // Options virtual, @@ -606,7 +608,7 @@ const TreeSelect = React.forwardRef((props, ref) halfCheckedKeys: rawHalfCheckedValues, treeDefaultExpandAll, treeExpandedKeys, - // treeDefaultExpandedKeys, + treeDefaultExpandedKeys, // onTreeExpand, // treeIcon, // treeMotion, @@ -626,7 +628,7 @@ const TreeSelect = React.forwardRef((props, ref) rawHalfCheckedValues, treeDefaultExpandAll, treeExpandedKeys, - // treeDefaultExpandedKeys, + treeDefaultExpandedKeys, // onTreeExpand, // treeIcon, // treeMotion, diff --git a/tests/Select.spec.js b/tests/Select.spec.js index 128e1299..c5126ec6 100644 --- a/tests/Select.spec.js +++ b/tests/Select.spec.js @@ -1,4 +1,3 @@ -/* eslint-disable no-undef react/no-multi-comp */ import React from 'react'; import { mount } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; diff --git a/tests/__snapshots__/Select.spec.js.snap b/tests/__snapshots__/Select.spec.js.snap index 00a90498..a49e3f95 100644 --- a/tests/__snapshots__/Select.spec.js.snap +++ b/tests/__snapshots__/Select.spec.js.snap @@ -432,7 +432,6 @@ exports[`TreeSelect.basic render renders correctly 1`] = ` style="width: 0px;" > Date: Tue, 14 Dec 2021 14:25:50 +0800 Subject: [PATCH 25/37] test: More snapshot --- tests/__snapshots__/Select.spec.js.snap | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/__snapshots__/Select.spec.js.snap b/tests/__snapshots__/Select.spec.js.snap index a49e3f95..e618ad7c 100644 --- a/tests/__snapshots__/Select.spec.js.snap +++ b/tests/__snapshots__/Select.spec.js.snap @@ -11,7 +11,6 @@ exports[`TreeSelect.basic render renders TreeNode correctly 1`] = ` class="rc-tree-select-selection-search" > Date: Tue, 14 Dec 2021 14:46:54 +0800 Subject: [PATCH 26/37] test: more snapshot --- examples/debug.tsx | 49 ++++++------------------- package.json | 2 +- src/TreeSelect.tsx | 1 + tests/Select.spec.js | 2 +- tests/__snapshots__/Select.spec.js.snap | 13 +++++-- 5 files changed, 23 insertions(+), 44 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index 3b1c7874..dd4326ca 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -8,44 +8,17 @@ const { TreeNode, SHOW_ALL, SHOW_CHILD } = TreeSelect; const SelectNode = TreeNode; const treeData = [ - { - key: 'P001', - title: 'P001', - value: 'P001', - children: [ - { - key: '0020', - title: '0020', - value: '0020', - children: [{ key: '9459', title: '9459', value: '9459' }], - }, - ], - }, - { - key: 'P002', - title: 'P002', - value: 'P002', - children: [ - { - key: '0021', - title: '0021', - value: '0021', - children: [{ key: '9458', title: '9458', value: '9458' }], - }, - ], - }, + { key: 'a', value: 'a', title: 'labela' }, + { key: 'b', value: 'b', title: 'labelb' }, ]; -function filterTreeNode(input, child) { - return String(child.props.title).indexOf(input) !== -1; -} +const createSelect = props => ; -export default () => ( - - - - - - - -); +export default () => + createSelect({ + // searchValue: 'a', + open: true, + treeDefaultExpandAll: true, + filterTreeNode: false, + placeholder: 'no no no no no no' + }); diff --git a/package.json b/package.json index 9d7035a1..2b0a7c8e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.0.0-alpha.4", + "rc-select": "~14.0.0-alpha.6", "rc-tree": "~5.3.2", "rc-util": "^5.16.1" } diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 4d29d5b0..addc06cf 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -660,6 +660,7 @@ const TreeSelect = React.forwardRef((props, ref) onSearch={onInternalSearch} // >>> Options OptionList={OptionList} + emptyOptions={!mergedTreeData.length} /> diff --git a/tests/Select.spec.js b/tests/Select.spec.js index c5126ec6..e29bc628 100644 --- a/tests/Select.spec.js +++ b/tests/Select.spec.js @@ -326,7 +326,7 @@ describe('TreeSelect.basic', () => { wrapper.selectNode(); wrapper.clearAll(); - expect(wrapper.find('Select').props().value).toHaveLength(0); + expect(wrapper.find('BaseSelect').prop('displayValues')).toHaveLength(0); }); it('has inputValue prop', () => { diff --git a/tests/__snapshots__/Select.spec.js.snap b/tests/__snapshots__/Select.spec.js.snap index e618ad7c..4f92ec17 100644 --- a/tests/__snapshots__/Select.spec.js.snap +++ b/tests/__snapshots__/Select.spec.js.snap @@ -717,7 +717,6 @@ exports[`TreeSelect.basic search nodes check tree changed by filter 1`] = ` class="rc-tree-select-selection-search" > +
@@ -978,7 +981,6 @@ exports[`TreeSelect.basic search nodes filter node but not remove then 1`] = ` class="rc-tree-select-selection-search" > +