+
{activeEntity && open && (
- {activeEntity.data.value}
+ {activeEntity.node.value}
)}
@@ -252,9 +209,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}
@@ -267,7 +224,7 @@ const OptionList: React.RefForwardingComponent<
checkable={checkable}
checkStrictly
checkedKeys={mergedCheckedKeys}
- selectedKeys={!checkable ? valueKeys : []}
+ selectedKeys={!checkable ? checkedKeys : []}
defaultExpandAll={treeDefaultExpandAll}
{...treeProps}
// Proxy event out
@@ -282,9 +239,7 @@ const OptionList: React.RefForwardingComponent<
);
};
-const RefOptionList = React.forwardRef
>(
- OptionList,
-);
+const RefOptionList = React.forwardRef(OptionList);
RefOptionList.displayName = 'OptionList';
export default RefOptionList;
diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx
index d92fc201..7d2dc6ef 100644
--- a/src/TreeSelect.tsx
+++ b/src/TreeSelect.tsx
@@ -1,8 +1,730 @@
-import generate, { TreeSelectProps } from './generate';
+import * as React from 'react';
+import { BaseSelect } from 'rc-select';
+import type { IconType } from 'rc-tree/lib/interface';
+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';
import OptionList from './OptionList';
+import TreeNode from './TreeNode';
+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';
+import LegacyContext from './LegacyContext';
+import useTreeData from './hooks/useTreeData';
+import { toArray, fillFieldNames, isNil } from './utils/valueUtil';
+import useCache from './hooks/useCache';
+import useRefFunc from './hooks/useRefFunc';
+import useDataEntities from './hooks/useDataEntities';
+import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil';
+import useCheckedKeys from './hooks/useCheckedKeys';
+import useFilterTreeData from './hooks/useFilterTreeData';
+import warningProps from './utils/warningPropsUtil';
+import warning from 'rc-util/lib/warning';
-const TreeSelect = generate({ prefixCls: 'rc-tree-select', optionList: OptionList as any });
+export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
-export { TreeSelectProps };
+export type RawValueType = string | number;
-export default TreeSelect;
+export interface LabeledValueType {
+ key?: React.Key;
+ value?: RawValueType;
+ label?: React.ReactNode;
+ /** Only works on `treeCheckStrictly` */
+ 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. */
+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;
+ children?: string;
+}
+
+export interface InternalFieldName extends Omit {
+ _title: 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 Omit {
+ prefixCls?: string;
+ id?: string;
+
+ // >>> Value
+ value?: ValueType;
+ defaultValue?: ValueType;
+ onChange?: (value: ValueType, labelList: React.ReactNode[], extra: ChangeEventExtra) => void;
+
+ // >>> Search
+ searchValue?: string;
+ /** @deprecated Use `searchValue` instead */
+ inputValue?: string;
+ onSearch?: (value: string) => void;
+ autoClearSearchValue?: boolean;
+ filterTreeNode?: boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean);
+ treeNodeFilterProp?: string;
+
+ // >>> Select
+ onSelect?: SelectProps['onSelect'];
+ onDeselect?: SelectProps['onDeselect'];
+
+ // >>> Selector
+ showCheckedStrategy?: CheckedStrategy;
+ treeNodeLabelProp?: string;
+
+ // >>> Field Names
+ fieldNames?: FieldNames;
+
+ // >>> Mode
+ multiple?: boolean;
+ treeCheckable?: boolean | React.ReactNode;
+ treeCheckStrictly?: boolean;
+ labelInValue?: boolean;
+
+ // >>> Data
+ treeData?: OptionType[];
+ treeDataSimpleMode?: boolean | SimpleModeConfig;
+ loadData?: (dataNode: LegacyDataNode) => Promise;
+ treeLoadedKeys?: React.Key[];
+ onTreeLoad?: (loadedKeys: React.Key[]) => void;
+
+ // >>> Expanded
+ treeDefaultExpandAll?: boolean;
+ treeExpandedKeys?: React.Key[];
+ treeDefaultExpandedKeys?: React.Key[];
+ onTreeExpand?: (expandedKeys: React.Key[]) => void;
+
+ // >>> Options
+ virtual?: boolean;
+ listHeight?: number;
+ listItemHeight?: number;
+ onDropdownVisibleChange?: (open: boolean) => void;
+
+ // >>> Tree
+ treeLine?: boolean;
+ treeIcon?: IconType;
+ showTreeIcon?: boolean;
+ switcherIcon?: IconType;
+ treeMotion?: any;
+}
+
+function isRawValue(value: RawValueType | LabeledValueType): value is RawValueType {
+ return !value || typeof value !== 'object';
+}
+
+const TreeSelect = React.forwardRef((props, ref) => {
+ const {
+ id,
+ prefixCls = 'rc-tree-select',
+
+ // Value
+ value,
+ defaultValue,
+ onChange,
+ onSelect,
+ onDeselect,
+
+ // Search
+ searchValue,
+ inputValue,
+ onSearch,
+ autoClearSearchValue = true,
+ filterTreeNode,
+ treeNodeFilterProp = 'value',
+
+ // Selector
+ showCheckedStrategy = SHOW_CHILD,
+ treeNodeLabelProp,
+
+ // Mode
+ multiple,
+ treeCheckable,
+ treeCheckStrictly,
+ labelInValue,
+
+ // FieldNames
+ fieldNames,
+
+ // Data
+ treeDataSimpleMode,
+ treeData,
+ children,
+ loadData,
+ treeLoadedKeys,
+ onTreeLoad,
+
+ // Expanded
+ treeDefaultExpandAll,
+ treeExpandedKeys,
+ treeDefaultExpandedKeys,
+ onTreeExpand,
+
+ // Options
+ virtual,
+ listHeight = 200,
+ listItemHeight = 20,
+ onDropdownVisibleChange,
+
+ // Tree
+ treeLine,
+ treeIcon,
+ showTreeIcon,
+ switcherIcon,
+ treeMotion,
+ } = props;
+
+ const mergedId = useId(id);
+ const treeConduction = treeCheckable && !treeCheckStrictly;
+ const mergedCheckable: boolean = !!(treeCheckable || treeCheckStrictly);
+ const mergedLabelInValue = treeCheckStrictly || labelInValue;
+ const mergedMultiple = mergedCheckable || multiple;
+
+ // ========================== Warning ===========================
+ if (process.env.NODE_ENV !== 'production') {
+ warningProps(props);
+ }
+
+ // ========================= FieldNames =========================
+ const mergedFieldNames: InternalFieldName = React.useMemo(
+ () => fillFieldNames(fieldNames),
+ /* eslint-disable react-hooks/exhaustive-deps */
+ [JSON.stringify(fieldNames)],
+ /* 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.
+ // Do not do anything to loop the data.
+ const mergedTreeData = useTreeData(treeData, children, treeDataSimpleMode);
+
+ const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames);
+
+ /** 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],
+ );
+
+ // Filtered Tree
+ const filteredTreeData = useFilterTreeData(mergedTreeData, mergedSearchValue, {
+ fieldNames: mergedFieldNames,
+ treeNodeFilterProp,
+ filterTreeNode,
+ });
+
+ // =========================== 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) {
+ const title = item[titleList[i]];
+ if (title !== undefined) {
+ return title;
+ }
+ }
+ }
+ },
+ [mergedFieldNames, treeNodeLabelProp],
+ );
+
+ // ========================= 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 = toLabeledValues(draftValues);
+
+ return values.map(item => {
+ let { label: rawLabel } = item;
+ const { value: rawValue, halfChecked: rawHalfChecked } = item;
+
+ let rawDisabled: boolean | undefined;
+
+ 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, toLabeledValues],
+ );
+
+ // =========================== Values ===========================
+ const [internalValue, setInternalValue] = useMergedState(defaultValue, { value });
+
+ const rawMixedLabeledValues = React.useMemo(
+ () => toLabeledValues(internalValue),
+ [toLabeledValues, internalValue],
+ );
+
+ // Split value into full check and half check
+ const [rawLabeledValues, rawHalfLabeledValues] = 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(
+ () => rawLabeledValues.map(item => item.value),
+ [rawLabeledValues],
+ );
+
+ // Convert value to key. Will fill missed keys for conduct check.
+ const [rawCheckedValues, rawHalfCheckedValues] = useCheckedKeys(
+ rawLabeledValues,
+ rawHalfLabeledValues,
+ treeConduction,
+ keyEntities,
+ );
+
+ // Convert rawCheckedKeys to check strategy related values
+ const displayValues = React.useMemo(() => {
+ // Collect keys which need to show
+ const displayKeys = formatStrategyValues(
+ rawCheckedValues,
+ showCheckedStrategy,
+ keyEntities,
+ mergedFieldNames,
+ );
+
+ // Convert to value and filled with label
+ const values = displayKeys.map(key => keyEntities[key]?.node?.[mergedFieldNames.value] ?? key);
+ const rawDisplayValues = convert2LabelValues(values);
+
+ const firstVal = rawDisplayValues[0];
+
+ if (!mergedMultiple && firstVal && isNil(firstVal.value) && isNil(firstVal.label)) {
+ return [];
+ }
+
+ return rawDisplayValues.map(item => ({
+ ...item,
+ label: item.label ?? item.value,
+ }));
+ }, [
+ mergedFieldNames,
+ mergedMultiple,
+ rawCheckedValues,
+ convert2LabelValues,
+ showCheckedStrategy,
+ keyEntities,
+ ]);
+
+ const [cachedDisplayValues] = useCache(displayValues);
+
+ // =========================== Change ===========================
+ const triggerChange = useRefFunc(
+ (
+ newRawValues: RawValueType[],
+ extra: { triggerValue?: RawValueType; selected?: boolean },
+ source: SelectSource,
+ ) => {
+ 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) {
+ let eventValues: RawValueType[] = newRawValues;
+ if (treeConduction) {
+ const formattedKeyList = formatStrategyValues(
+ newRawValues,
+ showCheckedStrategy,
+ keyEntities,
+ mergedFieldNames,
+ );
+ 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 returnRawValues: (LabeledValueType | RawValueType)[] = eventValues;
+
+ // We need fill half check back
+ if (treeCheckStrictly) {
+ const halfValues = rawHalfLabeledValues.filter(item => !eventValues.includes(item.value));
+
+ returnRawValues = [...returnRawValues, ...halfValues];
+ }
+
+ const returnLabeledValues = convert2LabelValues(returnRawValues);
+ 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;
+ }
+
+ fillAdditionalInfo(
+ additionalInfo,
+ triggerValue,
+ newRawValues,
+ mergedTreeData,
+ showPosition,
+ mergedFieldNames,
+ );
+
+ if (mergedCheckable) {
+ additionalInfo.checked = selected;
+ } else {
+ additionalInfo.selected = selected;
+ }
+
+ const returnValues = mergedLabelInValue
+ ? returnLabeledValues
+ : returnLabeledValues.map(item => item.value);
+
+ onChange(
+ mergedMultiple ? returnValues : returnValues[0],
+ mergedLabelInValue ? null : returnLabeledValues.map(item => item.label),
+ additionalInfo,
+ );
+ }
+ },
+ );
+
+ // ========================== Options ===========================
+ /** Trigger by option list */
+ const onOptionSelect = React.useCallback(
+ (selectedKey: React.Key, { selected, source }: { selected: boolean; source: SelectSource }) => {
+ const entity = keyEntities[selectedKey];
+ const node = entity?.node;
+ const selectedValue = node?.[mergedFieldNames.value] ?? selectedKey;
+
+ // Never be falsy but keep it safe
+ if (!mergedMultiple) {
+ // Single mode always set value
+ triggerChange([selectedValue], { selected: true, triggerValue: selectedValue }, 'option');
+ } else {
+ let newRawValues = selected
+ ? [...rawValues, selectedValue]
+ : rawCheckedValues.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);
+
+ // Conduction by selected or not
+ let checkedKeys: React.Key[];
+ if (selected) {
+ ({ checkedKeys } = conductCheck(keyList, true, keyEntities));
+ } else {
+ ({ checkedKeys } = conductCheck(
+ keyList,
+ { checked: false, halfCheckedKeys: rawHalfCheckedValues },
+ keyEntities,
+ ));
+ }
+
+ // Fill back of keys
+ newRawValues = [
+ ...missingRawValues,
+ ...checkedKeys.map(key => keyEntities[key].node[mergedFieldNames.value]),
+ ];
+ }
+ triggerChange(newRawValues, { selected, triggerValue: selectedValue }, source || 'option');
+ }
+
+ // Trigger select event
+ if (selected || !mergedMultiple) {
+ onSelect?.(selectedValue, fillLegacyProps(node));
+ } else {
+ onDeselect?.(selectedValue, fillLegacyProps(node));
+ }
+ },
+ [
+ splitRawValues,
+ valueEntities,
+ keyEntities,
+ mergedFieldNames,
+ mergedMultiple,
+ rawValues,
+ triggerChange,
+ treeConduction,
+ onSelect,
+ onDeselect,
+ rawCheckedValues,
+ rawHalfCheckedValues,
+ ],
+ );
+
+ // ========================== Dropdown ==========================
+ const onInternalDropdownVisibleChange = React.useCallback(
+ (open: boolean) => {
+ if (onDropdownVisibleChange) {
+ const legacyParam = {};
+
+ Object.defineProperty(legacyParam, 'documentClickClose', {
+ get() {
+ warning(false, 'Second param of `onDropdownVisibleChange` has been removed.');
+ return false;
+ },
+ });
+
+ (onDropdownVisibleChange as any)(open, legacyParam);
+ }
+ },
+ [onDropdownVisibleChange],
+ );
+
+ // ====================== 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, source: 'selection' });
+ }
+ },
+ );
+
+ // ========================== Context ===========================
+ const treeSelectContext = React.useMemo(
+ () => ({
+ virtual,
+ listHeight,
+ listItemHeight,
+ treeData: filteredTreeData,
+ fieldNames: mergedFieldNames,
+ onSelect: onOptionSelect,
+ }),
+ [virtual, listHeight, listItemHeight, filteredTreeData, mergedFieldNames, onOptionSelect],
+ );
+
+ // ======================= Legacy Context =======================
+ const legacyContext = React.useMemo(
+ () => ({
+ checkable: mergedCheckable,
+ loadData,
+ treeLoadedKeys,
+ onTreeLoad,
+ checkedKeys: rawCheckedValues,
+ halfCheckedKeys: rawHalfCheckedValues,
+ treeDefaultExpandAll,
+ treeExpandedKeys,
+ treeDefaultExpandedKeys,
+ onTreeExpand,
+ treeIcon,
+ treeMotion,
+ showTreeIcon,
+ switcherIcon,
+ treeLine,
+ treeNodeFilterProp,
+ keyEntities,
+ }),
+ [
+ mergedCheckable,
+ loadData,
+ treeLoadedKeys,
+ onTreeLoad,
+ rawCheckedValues,
+ rawHalfCheckedValues,
+ treeDefaultExpandAll,
+ treeExpandedKeys,
+ treeDefaultExpandedKeys,
+ onTreeExpand,
+ treeIcon,
+ treeMotion,
+ showTreeIcon,
+ switcherIcon,
+ treeLine,
+ treeNodeFilterProp,
+ keyEntities,
+ ],
+ );
+
+ // =========================== Render ===========================
+ return (
+
+
+ >> MISC
+ id={mergedId}
+ prefixCls={prefixCls}
+ mode={mergedMultiple ? 'multiple' : undefined}
+ // >>> Display Value
+ displayValues={cachedDisplayValues}
+ onDisplayValuesChange={onDisplayValuesChange}
+ // >>> Search
+ searchValue={mergedSearchValue}
+ onSearch={onInternalSearch}
+ // >>> Options
+ OptionList={OptionList}
+ emptyOptions={!mergedTreeData.length}
+ onDropdownVisibleChange={onInternalDropdownVisibleChange}
+ />
+
+
+ );
+});
+
+// Assign name for Debug
+if (process.env.NODE_ENV !== 'production') {
+ TreeSelect.displayName = 'TreeSelect';
+}
+
+const GenericTreeSelect = TreeSelect as unknown as (<
+ Values extends BaseOptionType | DefaultOptionType = DefaultOptionType,
+>(
+ props: React.PropsWithChildren> & {
+ ref?: React.Ref;
+ },
+) => React.ReactElement) & {
+ TreeNode: typeof TreeNode;
+ SHOW_ALL: typeof SHOW_ALL;
+ SHOW_PARENT: typeof SHOW_PARENT;
+ SHOW_CHILD: typeof SHOW_CHILD;
+};
+
+GenericTreeSelect.TreeNode = TreeNode;
+GenericTreeSelect.SHOW_ALL = SHOW_ALL;
+GenericTreeSelect.SHOW_PARENT = SHOW_PARENT;
+GenericTreeSelect.SHOW_CHILD = SHOW_CHILD;
+
+export default GenericTreeSelect;
diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts
new file mode 100644
index 00000000..2157044d
--- /dev/null
+++ b/src/TreeSelectContext.ts
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect';
+
+export interface TreeSelectContextProps {
+ virtual?: boolean;
+ listHeight: number;
+ listItemHeight: number;
+ treeData: DefaultOptionType[];
+ fieldNames: InternalFieldName;
+ onSelect: OnInternalSelect;
+}
+
+const TreeSelectContext = React.createContext(null as any);
+
+export default TreeSelectContext;
diff --git a/src/generate.tsx b/src/generate.tsx
deleted file mode 100644
index 64d06c6c..00000000
--- a/src/generate.tsx
+++ /dev/null
@@ -1,634 +0,0 @@
-import * as React from 'react';
-import { useMemo } from 'react';
-import type { SelectProps, RefSelectProps, GenerateConfig } from 'rc-select/lib/generate';
-import generateSelector from 'rc-select/lib/generate';
-import { getLabeledValue } from 'rc-select/lib/utils/valueUtil';
-import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil';
-import { conductCheck } from 'rc-tree/lib/utils/conductUtil';
-import type { IconType } from 'rc-tree/lib/interface';
-import omit from 'rc-util/lib/omit';
-import type { FilterFunc } from 'rc-select/lib/interface/generator';
-import { INTERNAL_PROPS_MARK } from 'rc-select/lib/interface/generator';
-import useMergedState from 'rc-util/lib/hooks/useMergedState';
-import warning from 'rc-util/lib/warning';
-import TreeNode from './TreeNode';
-import type {
- Key,
- DefaultValueType,
- DataNode,
- LabelValueType,
- SimpleModeConfig,
- RawValueType,
- ChangeEventExtra,
- LegacyDataNode,
- SelectSource,
- FlattenDataNode,
- FieldNames,
-} from './interface';
-import {
- flattenOptions,
- filterOptions,
- isValueDisabled,
- findValueOption,
- addValue,
- removeValue,
- getRawValueLabeled,
- toArray,
- fillFieldNames,
-} from './utils/valueUtil';
-import warningProps from './utils/warningPropsUtil';
-import { SelectContext } from './Context';
-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 { fillAdditionalInfo } from './utils/legacyUtil';
-import useSelectValues from './hooks/useSelectValues';
-
-const OMIT_PROPS: (keyof TreeSelectProps)[] = [
- 'expandedKeys' as any,
- 'treeData',
- 'treeCheckable',
- 'showCheckedStrategy',
- 'searchPlaceholder',
- 'treeLine',
- 'treeIcon',
- 'showTreeIcon',
- 'switcherIcon',
- 'treeNodeFilterProp',
- 'filterTreeNode',
- 'dropdownPopupAlign',
- 'treeDefaultExpandAll',
- 'treeCheckStrictly',
- 'treeExpandedKeys',
- 'treeLoadedKeys',
- 'treeMotion',
- 'onTreeExpand',
- 'onTreeLoad',
- 'labelRender',
- 'loadData',
- 'treeDataSimpleMode',
- 'treeNodeLabelProp',
- 'treeDefaultExpandedKeys',
-];
-
-export interface TreeSelectProps
- extends Omit<
- SelectProps,
- | 'onChange'
- | 'mode'
- | 'menuItemSelectedIcon'
- | 'dropdownAlign'
- | 'backfill'
- | 'getInputElement'
- | 'optionLabelProp'
- | 'tokenSeparators'
- | 'filterOption'
- | 'fieldNames'
- > {
- multiple?: boolean;
- showArrow?: boolean;
- showSearch?: boolean;
- open?: boolean;
- defaultOpen?: boolean;
- value?: ValueType;
- defaultValue?: ValueType;
- disabled?: boolean;
-
- placeholder?: React.ReactNode;
- /** @deprecated Use `searchValue` instead */
- inputValue?: string;
- searchValue?: string;
- autoClearSearchValue?: boolean;
-
- maxTagPlaceholder?: (omittedValues: LabelValueType[]) => React.ReactNode;
-
- fieldNames?: FieldNames;
- loadData?: (dataNode: LegacyDataNode) => Promise;
- treeNodeFilterProp?: string;
- treeNodeLabelProp?: string;
- treeDataSimpleMode?: boolean | SimpleModeConfig;
- treeExpandedKeys?: Key[];
- treeDefaultExpandedKeys?: Key[];
- treeLoadedKeys?: Key[];
- treeCheckable?: boolean | React.ReactNode;
- treeCheckStrictly?: boolean;
- showCheckedStrategy?: CheckedStrategy;
- treeDefaultExpandAll?: boolean;
- treeData?: DataNode[];
- treeLine?: boolean;
- treeIcon?: IconType;
- showTreeIcon?: boolean;
- switcherIcon?: IconType;
- treeMotion?: any;
- children?: React.ReactNode;
-
- filterTreeNode?: boolean | FilterFunc;
- dropdownPopupAlign?: any;
-
- // Event
- onSearch?: (value: string) => void;
- onChange?: (value: ValueType, labelList: React.ReactNode[], extra: ChangeEventExtra) => void;
- onTreeExpand?: (expandedKeys: Key[]) => void;
- onTreeLoad?: (loadedKeys: Key[]) => void;
- onDropdownVisibleChange?: (open: boolean) => void;
-
- // Legacy
- /** `searchPlaceholder` has been removed since search box has been merged into input box */
- searchPlaceholder?: React.ReactNode;
-
- /** @private This is not standard API since we only used in `rc-cascader`. Do not use in your production */
- labelRender?: (entity: FlattenDataNode, value: RawValueType) => React.ReactNode;
-}
-
-export default function generate(config: {
- prefixCls: string;
- optionList: GenerateConfig['components']['optionList'];
-}) {
- const { prefixCls, optionList } = config;
-
- const RefSelect = generateSelector({
- prefixCls,
- components: {
- optionList,
- },
- // Not use generate since we will handle ourself
- convertChildrenToData: () => null,
- flattenOptions,
- // Handle `optionLabelProp` in TreeSelect component
- getLabeledValue: getLabeledValue as any,
- filterOptions,
- isValueDisabled,
- findValueOption,
- omitDOMProps: (props: TreeSelectProps) => omit(props, OMIT_PROPS),
- });
-
- RefSelect.displayName = 'Select';
-
- // =================================================================================
- // = Tree =
- // =================================================================================
- const RefTreeSelect = React.forwardRef((props, ref) => {
- const {
- fieldNames,
- multiple,
- treeCheckable,
- treeCheckStrictly,
- showCheckedStrategy = 'SHOW_CHILD',
- labelInValue,
- loadData,
- treeLoadedKeys,
- treeNodeFilterProp = 'value',
- treeNodeLabelProp,
- treeDataSimpleMode,
- treeData,
- treeExpandedKeys,
- treeDefaultExpandedKeys,
- treeDefaultExpandAll,
- children,
- treeIcon,
- showTreeIcon,
- switcherIcon,
- treeLine,
- treeMotion,
- filterTreeNode,
- dropdownPopupAlign,
- onChange,
- onTreeExpand,
- onTreeLoad,
- onDropdownVisibleChange,
- onSelect,
- onDeselect,
- labelRender,
- } = props;
- const mergedCheckable: React.ReactNode | boolean = treeCheckable || treeCheckStrictly;
- const mergedMultiple = multiple || mergedCheckable;
- const treeConduction = treeCheckable && !treeCheckStrictly;
- const mergedLabelInValue = treeCheckStrictly || labelInValue;
-
- // ======================= Tree Data =======================
- // FieldNames
- const mergedFieldNames = fillFieldNames(fieldNames, true);
-
- // Legacy both support `label` or `title` if not set.
- // We have to fallback to function to handle this
- const getTreeNodeTitle = (node: DataNode): React.ReactNode => {
- if (!treeData) {
- return node.title;
- }
-
- if (mergedFieldNames?.label) {
- return node[mergedFieldNames.label];
- }
-
- return node.label || node.title;
- };
-
- const getTreeNodeLabelProp = (entity: FlattenDataNode, val: RawValueType): React.ReactNode => {
- if (labelRender) {
- return labelRender(entity, val);
- }
-
- // Skip since entity not exist
- if (!entity) {
- return undefined;
- }
-
- const { node } = entity.data;
-
- if (treeNodeLabelProp) {
- return node[treeNodeLabelProp];
- }
-
- return getTreeNodeTitle(node);
- };
-
- const mergedTreeData = useTreeData(treeData, children, {
- getLabelProp: getTreeNodeTitle,
- simpleMode: treeDataSimpleMode,
- fieldNames: mergedFieldNames,
- });
-
- const flattedOptions = useMemo(() => flattenOptions(mergedTreeData), [mergedTreeData]);
- const [cacheKeyMap, cacheValueMap] = useKeyValueMap(flattedOptions);
- const [getEntityByKey, getEntityByValue] = useKeyValueMapping(cacheKeyMap, cacheValueMap);
-
- // Only generate keyEntities for check conduction when is `treeCheckable`
- const { keyEntities: conductKeyEntities } = useMemo(() => {
- if (treeConduction) {
- return convertDataToEntities(mergedTreeData as any);
- }
- return { keyEntities: null };
- }, [mergedTreeData, treeCheckable, treeCheckStrictly]);
-
- // ========================== Ref ==========================
- const selectRef = React.useRef(null);
-
- React.useImperativeHandle(ref, () => ({
- scrollTo: selectRef.current.scrollTo,
- focus: selectRef.current.focus,
- blur: selectRef.current.blur,
-
- /** @private Internal usage. It's save to remove if `rc-cascader` not use it any longer */
- getEntityByValue,
- }));
-
- // ========================= Value =========================
- const [value, setValue] = useMergedState(props.defaultValue, {
- value: props.value,
- });
-
- /** Get `missingRawValues` which not exist in the tree yet */
- const splitRawValues = (newRawValues: RawValueType[]) => {
- const missingRawValues = [];
- const existRawValues = [];
-
- // Keep missing value in the cache
- newRawValues.forEach(val => {
- if (getEntityByValue(val)) {
- existRawValues.push(val);
- } else {
- missingRawValues.push(val);
- }
- });
-
- return { missingRawValues, existRawValues };
- };
-
- const [rawValues, rawHalfCheckedKeys]: [RawValueType[], RawValueType[]] = useMemo(() => {
- const valueHalfCheckedKeys: RawValueType[] = [];
- const newRawValues: RawValueType[] = [];
-
- toArray(value).forEach(item => {
- if (item && typeof item === 'object' && 'value' in item) {
- if (item.halfChecked && treeCheckStrictly) {
- const entity = getEntityByValue(item.value);
- valueHalfCheckedKeys.push(entity ? entity.key : item.value);
- } else {
- newRawValues.push(item.value);
- }
- } else {
- newRawValues.push(item as RawValueType);
- }
- });
-
- // We need do conduction of values
- if (treeConduction) {
- const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
- const keyList = existRawValues.map(val => getEntityByValue(val).key);
-
- const { checkedKeys, halfCheckedKeys } = conductCheck(keyList, true, conductKeyEntities);
- return [
- [...missingRawValues, ...checkedKeys.map(key => getEntityByKey(key).data.value)],
- halfCheckedKeys,
- ];
- }
- return [newRawValues, valueHalfCheckedKeys];
- }, [
- value,
- flattedOptions,
- mergedMultiple,
- mergedLabelInValue,
- treeCheckable,
- treeCheckStrictly,
- ]);
-
- const selectValues = useSelectValues(rawValues, {
- treeConduction,
- value,
- showCheckedStrategy,
- conductKeyEntities,
- getEntityByValue,
- getEntityByKey,
- getLabelProp: getTreeNodeLabelProp,
- });
-
- const triggerChange = (
- newRawValues: RawValueType[],
- extra: { triggerValue: RawValueType; selected: boolean },
- source: SelectSource,
- ) => {
- setValue(mergedMultiple ? newRawValues : newRawValues[0]);
- if (onChange) {
- let eventValues: RawValueType[] = newRawValues;
- if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') {
- const keyList = newRawValues.map(val => {
- const entity = getEntityByValue(val);
- return entity ? entity.key : val;
- });
- const formattedKeyList = formatStrategyKeys(
- keyList,
- showCheckedStrategy,
- conductKeyEntities,
- );
-
- eventValues = formattedKeyList.map(key => {
- const entity = getEntityByKey(key);
- return entity ? entity.data.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,
- );
- }
- };
-
- const onInternalSelect = (
- selectValue: RawValueType,
- option: DataNode,
- source: SelectSource,
- ) => {
- const eventValue = mergedLabelInValue ? selectValue : selectValue;
-
- if (!mergedMultiple) {
- // Single mode always set value
- triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source);
- } else {
- let newRawValues = addValue(rawValues, selectValue);
-
- // Add keys if tree conduction
- if (treeConduction) {
- // Should keep missing values
- const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
- const keyList = existRawValues.map(val => getEntityByValue(val).key);
- const { checkedKeys } = conductCheck(keyList, true, conductKeyEntities);
- newRawValues = [
- ...missingRawValues,
- ...checkedKeys.map(key => getEntityByKey(key).data.value),
- ];
- }
-
- triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source);
- }
-
- if (onSelect) {
- onSelect(eventValue, option);
- }
- };
-
- const onInternalDeselect = (
- selectValue: RawValueType,
- option: DataNode,
- source: SelectSource,
- ) => {
- const eventValue = mergedLabelInValue ? selectValue : selectValue;
-
- let newRawValues = removeValue(rawValues, selectValue);
-
- // Remove keys if tree conduction
- if (treeConduction) {
- const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
- const keyList = existRawValues.map(val => getEntityByValue(val).key);
- const { checkedKeys } = conductCheck(
- keyList,
- { checked: false, halfCheckedKeys: rawHalfCheckedKeys },
- conductKeyEntities,
- );
- newRawValues = [
- ...missingRawValues,
- ...checkedKeys.map(key => getEntityByKey(key).data.value),
- ];
- }
-
- triggerChange(newRawValues, { selected: false, triggerValue: selectValue }, source);
-
- if (onDeselect) {
- onDeselect(eventValue, option);
- }
- };
-
- const onInternalClear = () => {
- triggerChange([], null, 'clear');
- };
-
- // ========================= Open ==========================
- const onInternalDropdownVisibleChange = React.useCallback(
- (open: boolean) => {
- if (onDropdownVisibleChange) {
- const legacyParam = {};
-
- Object.defineProperty(legacyParam, 'documentClickClose', {
- get() {
- warning(false, 'Second param of `onDropdownVisibleChange` has been removed.');
- return false;
- },
- });
-
- (onDropdownVisibleChange as any)(open, legacyParam);
- }
- },
- [onDropdownVisibleChange],
- );
-
- // ======================== Warning ========================
- if (process.env.NODE_ENV !== 'production') {
- warningProps(props);
- }
-
- // ======================== Render =========================
- // We pass some props into select props style
- const selectProps: Partial> = {
- optionLabelProp: null,
- optionFilterProp: treeNodeFilterProp,
- dropdownAlign: dropdownPopupAlign,
- internalProps: {
- mark: INTERNAL_PROPS_MARK,
- onClear: onInternalClear,
- skipTriggerChange: true,
- skipTriggerSelect: true,
- onRawSelect: onInternalSelect,
- onRawDeselect: onInternalDeselect,
- },
- };
-
- if ('filterTreeNode' in props) {
- selectProps.filterOption = filterTreeNode;
- }
-
- const selectContext = 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,
- ],
- );
-
- return (
-
-
-
- );
- });
-
- RefTreeSelect.displayName = 'TreeSelect';
-
- // =================================================================================
- // = Generic =
- // =================================================================================
- const TreeSelect = RefTreeSelect as any as ((
- props: React.PropsWithChildren> & {
- ref?: React.Ref;
- },
- ) => React.ReactElement) & {
- TreeNode: typeof TreeNode;
- SHOW_ALL: typeof SHOW_ALL;
- SHOW_PARENT: typeof SHOW_PARENT;
- SHOW_CHILD: typeof SHOW_CHILD;
- };
-
- TreeSelect.TreeNode = TreeNode;
- TreeSelect.SHOW_ALL = SHOW_ALL;
- TreeSelect.SHOW_PARENT = SHOW_PARENT;
- TreeSelect.SHOW_CHILD = SHOW_CHILD;
-
- return TreeSelect;
-}
diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts
new file mode 100644
index 00000000..db939e5f
--- /dev/null
+++ b/src/hooks/useCache.ts
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import type { 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 (values: LabeledValueType[]): [LabeledValueType[]] => {
+ const cacheRef = React.useRef({
+ valueLabels: new Map(),
+ });
+
+ return React.useMemo(() => {
+ const { valueLabels } = cacheRef.current;
+ const valueLabelsCache = new Map();
+
+ const filledValues = values.map(item => {
+ const { value } = item;
+ const mergedLabel = item.label ?? valueLabels.get(value);
+
+ // Save in cache
+ valueLabelsCache.set(value, mergedLabel);
+
+ return {
+ ...item,
+ label: mergedLabel,
+ };
+ });
+
+ cacheRef.current.valueLabels = valueLabelsCache;
+
+ return [filledValues];
+ }, [values]);
+};
diff --git a/src/hooks/useCheckedKeys.ts b/src/hooks/useCheckedKeys.ts
new file mode 100644
index 00000000..de9a97d1
--- /dev/null
+++ b/src/hooks/useCheckedKeys.ts
@@ -0,0 +1,27 @@
+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);
+
+ const missingValues = checkedKeys.filter(key => !keyEntities[key]);
+
+ if (treeConduction) {
+ ({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities));
+ }
+
+ return [
+ // Checked keys should fill with missing keys which should de-duplicated
+ Array.from(new Set([...missingValues, ...checkedKeys])),
+ // Half checked keys
+ halfCheckedKeys];
+ }, [rawLabeledValues, rawHalfCheckedValues, treeConduction, keyEntities]);
diff --git a/src/hooks/useDataEntities.ts b/src/hooks/useDataEntities.ts
new file mode 100644
index 00000000..6e77011b
--- /dev/null
+++ b/src/hooks/useDataEntities.ts
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil';
+import type { DataEntity } from 'rc-tree/lib/interface';
+import type { FieldNames, RawValueType } from '../TreeSelect';
+import warning from 'rc-util/lib/warning';
+import { isNil } from '../utils/valueUtil';
+
+export default (treeData: any, fieldNames: FieldNames) =>
+ React.useMemo<{ keyEntities: Record }>(() => {
+ const collection = convertDataToEntities(treeData, {
+ fieldNames,
+ initWrapper: wrapper => ({
+ ...wrapper,
+ valueEntities: new Map(),
+ }),
+ processEntity: (entity, wrapper: any) => {
+ const val = entity.node[fieldNames.value];
+
+ // Check if exist same value
+ if (process.env.NODE_ENV !== 'production') {
+ const key = entity.node.key;
+
+ warning(!isNil(val), 'TreeNode `value` is invalidate: undefined');
+ warning(!wrapper.valueEntities.has(val), `Same \`value\` exist in the tree: ${val}`);
+ warning(
+ !key || String(key) === String(val),
+ `\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${val}.`,
+ );
+ }
+ wrapper.valueEntities.set(val, entity);
+ },
+ });
+
+ return collection;
+ }, [treeData, fieldNames]) as ReturnType & {
+ valueEntities: Map;
+ };
diff --git a/src/hooks/useFilterTreeData.ts b/src/hooks/useFilterTreeData.ts
new file mode 100644
index 00000000..016c573a
--- /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 || filterTreeNode === false) {
+ 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/src/hooks/useKeyValueMap.ts b/src/hooks/useKeyValueMap.ts
deleted file mode 100644
index 09a7415c..00000000
--- a/src/hooks/useKeyValueMap.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as React from 'react';
-import type { FlattenDataNode, Key, RawValueType } from '../interface';
-
-/**
- * Return cached Key Value map with DataNode.
- * Only re-calculate when `flattenOptions` changed.
- */
-export default function useKeyValueMap(flattenOptions: FlattenDataNode[]) {
- return React.useMemo(() => {
- const cacheKeyMap: Map = new Map();
- const cacheValueMap: Map = new Map();
-
- // Cache options by key
- flattenOptions.forEach((dataNode: FlattenDataNode) => {
- cacheKeyMap.set(dataNode.key, dataNode);
- cacheValueMap.set(dataNode.data.value, dataNode);
- });
-
- return [cacheKeyMap, cacheValueMap];
- }, [flattenOptions]);
-}
diff --git a/src/hooks/useKeyValueMapping.ts b/src/hooks/useKeyValueMapping.ts
deleted file mode 100644
index 4882cd30..00000000
--- a/src/hooks/useKeyValueMapping.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from 'react';
-import type { FlattenDataNode, Key, RawValueType } from '../interface';
-
-export type SkipType = null | 'select' | 'checkbox';
-
-export function isDisabled(dataNode: FlattenDataNode, skipType: SkipType): boolean {
- if (!dataNode) {
- return true;
- }
-
- const { disabled, disableCheckbox } = dataNode.data.node;
-
- switch (skipType) {
- case 'checkbox':
- return disabled || disableCheckbox;
-
- default:
- return disabled;
- }
-}
-
-export default function useKeyValueMapping(
- cacheKeyMap: Map,
- cacheValueMap: Map,
-): [
- (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
- (value: RawValueType, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
-] {
- const getEntityByKey = React.useCallback(
- (key: Key, skipType: SkipType = 'select', ignoreDisabledCheck?: boolean) => {
- const dataNode = cacheKeyMap.get(key);
-
- if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
- return null;
- }
-
- return dataNode;
- },
- [cacheKeyMap],
- );
-
- const getEntityByValue = React.useCallback(
- (value: RawValueType, skipType: SkipType = 'select', ignoreDisabledCheck?: boolean) => {
- const dataNode = cacheValueMap.get(value);
-
- if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
- return null;
- }
-
- return dataNode;
- },
- [cacheValueMap],
- );
-
- return [getEntityByKey, getEntityByValue];
-}
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/useSelectValues.ts b/src/hooks/useSelectValues.ts
deleted file mode 100644
index b2e55003..00000000
--- a/src/hooks/useSelectValues.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as React from 'react';
-import type { DefaultValueType } from 'rc-select/lib/interface/generator';
-import type { DataEntity } from 'rc-tree/lib/interface';
-import type { RawValueType, FlattenDataNode, Key, LabelValueType } from '../interface';
-import type { SkipType } from './useKeyValueMapping';
-import { getRawValueLabeled } from '../utils/valueUtil';
-import type { CheckedStrategy } from '../utils/strategyUtil';
-import { formatStrategyKeys } from '../utils/strategyUtil';
-
-interface Config {
- treeConduction: boolean;
- /** Current `value` of TreeSelect */
- value: DefaultValueType;
- showCheckedStrategy: CheckedStrategy;
- conductKeyEntities: Record;
- getEntityByKey: (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode;
- getEntityByValue: (
- value: RawValueType,
- skipType?: SkipType,
- ignoreDisabledCheck?: boolean,
- ) => FlattenDataNode;
- getLabelProp: (entity: FlattenDataNode, value: RawValueType) => React.ReactNode;
-}
-
-/** Return */
-export default function useSelectValues(
- rawValues: RawValueType[],
- {
- value,
- getEntityByValue,
- getEntityByKey,
- treeConduction,
- showCheckedStrategy,
- conductKeyEntities,
- getLabelProp,
- }: Config,
-): LabelValueType[] {
- return React.useMemo(() => {
- let mergedRawValues = rawValues;
-
- if (treeConduction) {
- const rawKeys = formatStrategyKeys(
- rawValues.map(val => {
- const entity = getEntityByValue(val);
- return entity ? entity.key : val;
- }),
- showCheckedStrategy,
- conductKeyEntities,
- );
-
- mergedRawValues = rawKeys.map(key => {
- const entity = getEntityByKey(key);
- return entity ? entity.data.value : key;
- });
- }
-
- return getRawValueLabeled(mergedRawValues, value, getEntityByValue, getLabelProp);
- }, [rawValues, value, treeConduction, showCheckedStrategy, getEntityByValue]);
-}
diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts
index 3102bf87..26ffbebd 100644
--- a/src/hooks/useTreeData.ts
+++ b/src/hooks/useTreeData.ts
@@ -1,15 +1,7 @@
import * as React from 'react';
-import warning from 'rc-util/lib/warning';
-import type {
- DataNode,
- InternalDataEntity,
- SimpleModeConfig,
- RawValueType,
- FieldNames,
-} from '../interface';
+import type { DataNode, SimpleModeConfig } from '../interface';
import { convertChildrenToData } from '../utils/legacyUtil';
-
-const MAX_WARNING_TIMES = 10;
+import type { DefaultOptionType } from '../TreeSelect';
function parseSimpleTreeData(
treeData: DataNode[],
@@ -47,70 +39,6 @@ function parseSimpleTreeData(
return rootNodeList;
}
-/**
- * Format `treeData` with `value` & `key` which is used for calculation
- */
-function formatTreeData(
- treeData: DataNode[],
- getLabelProp: (node: DataNode) => React.ReactNode,
- fieldNames: FieldNames,
-): InternalDataEntity[] {
- let warningTimes = 0;
- const valueSet = new Set();
-
- // Field names
- const { value: fieldValue, children: fieldChildren } = fieldNames;
-
- function dig(dataNodes: DataNode[]) {
- return (dataNodes || []).map(node => {
- const { key, children, ...restProps } = node;
-
- const value = node[fieldValue];
- const mergedValue = fieldValue in node ? value : key;
-
- const dataNode: InternalDataEntity = {
- ...restProps,
- key: key !== null && key !== undefined ? key : mergedValue,
- value: mergedValue,
- title: getLabelProp(node),
- node,
- };
-
- // Check `key` & `value` and warning user
- if (process.env.NODE_ENV !== 'production') {
- if (
- key !== null &&
- key !== undefined &&
- value !== undefined &&
- String(key) !== String(value) &&
- warningTimes < MAX_WARNING_TIMES
- ) {
- warningTimes += 1;
- warning(
- false,
- `\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${value}.`,
- );
- }
-
- warning(
- value !== undefined || key !== undefined,
- 'TreeNode `value` is invalidate: undefined',
- );
- warning(!valueSet.has(value), `Same \`value\` exist in the tree: ${value}`);
- valueSet.add(value);
- }
-
- if (node[fieldChildren] !== undefined) {
- dataNode.children = dig(node[fieldChildren]);
- }
-
- return dataNode;
- });
- }
-
- return dig(treeData);
-}
-
/**
* Convert `treeData` or `children` into formatted `treeData`.
* Will not re-calculate if `treeData` or `children` not change.
@@ -118,46 +46,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[] {
- 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);
- }
+ simpleMode: boolean | SimpleModeConfig,
+): DefaultOptionType[] {
+ return React.useMemo(() => {
+ if (treeData) {
+ return simpleMode
+ ? parseSimpleTreeData(treeData, {
+ id: 'id',
+ pId: 'pId',
+ rootPId: null,
+ ...(simpleMode !== true ? simpleMode : {}),
+ })
+ : treeData;
+ }
- return cacheRef.current.formatTreeData;
+ return convertChildrenToData(children);
+ }, [children, simpleMode, treeData]);
}
diff --git a/src/utils/legacyUtil.tsx b/src/utils/legacyUtil.tsx
index 878c53c2..328fd4f4 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)
@@ -40,7 +40,6 @@ export function convertChildrenToData(nodes: React.ReactNode): DataNode[] {
}
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {
- // Skip if not dataNode exist
if (!dataNode) {
return dataNode as LegacyDataNode;
}
@@ -66,23 +65,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;
}
diff --git a/src/utils/strategyUtil.ts b/src/utils/strategyUtil.ts
index 2ac256b6..4eacece8 100644
--- a/src/utils/strategyUtil.ts
+++ b/src/utils/strategyUtil.ts
@@ -1,5 +1,7 @@
+import type * as React from 'react';
+import type { InternalFieldName } from '../TreeSelect';
import type { DataEntity } from 'rc-tree/lib/interface';
-import type { RawValueType, Key, DataNode } from '../interface';
+import type { RawValueType, Key } from '../interface';
import { isCheckDisabled } from './valueUtil';
export const SHOW_ALL = 'SHOW_ALL';
@@ -8,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: 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;
@@ -32,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.node as DataNode).key)) {
+ if (parent && !isCheckDisabled(parent.node) && valueSet.has(parent.key)) {
return false;
}
return true;
});
}
- return keys;
+ return values;
}
diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts
index 2d43e9a7..b414b409 100644
--- a/src/utils/valueUtil.ts
+++ b/src/utils/valueUtil.ts
@@ -1,21 +1,6 @@
-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';
-import type {
- FlattenDataNode,
- Key,
- RawValueType,
- DataNode,
- DefaultValueType,
- LabelValueType,
- LegacyDataNode,
- FieldNames,
- InternalDataEntity,
-} from '../interface';
-import { fillLegacyProps } from './legacyUtil';
-import type { SkipType } from '../hooks/useKeyValueMapping';
-
-type CompatibleDataNode = Omit;
+import type * as React from 'react';
+import type { DataNode, FieldNames } from '../interface';
+import type { DefaultOptionType, InternalFieldName } from '../TreeSelect';
export function toArray(value: T | T[]): T[] {
if (Array.isArray(value)) {
@@ -24,228 +9,43 @@ export function toArray(value: T | T[]): T[] {
return value !== undefined ? [value] : [];
}
-/**
- * Fill `fieldNames` with default field names.
- *
- * @param fieldNames passed props
- * @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 fillFieldNames(fieldNames?: FieldNames) {
const { label, value, children } = fieldNames || {};
- const filledNames: FieldNames = {
- value: value || 'value',
+ const mergedValue = value || 'value';
+
+ return {
+ _title: label ? [label] : ['title', 'label'],
+ value: mergedValue,
+ key: mergedValue,
children: children || 'children',
};
-
- if (!skipTitle || label) {
- filledNames.label = label || 'label';
- }
-
- return filledNames;
-}
-
-export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] {
- const optionMap: Map = new Map();
-
- options.forEach(flattenItem => {
- const { data, value } = flattenItem;
- optionMap.set(value, data.node);
- });
-
- return values.map(val => fillLegacyProps(optionMap.get(val)));
-}
-
-export function isValueDisabled(value: RawValueType, options: CompatibleDataNode[]): boolean {
- const option = findValueOption([value], options)[0];
- if (option) {
- return option.disabled;
- }
-
- return false;
}
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 {
- key: Key;
- children?: TreeDataNode[];
-}
-
-function getLevel({ parent }: FlattenNode): number {
- let level = 0;
- let current = parent;
-
- while (current) {
- current = current.parent;
- level += 1;
- }
-
- return level;
-}
+/** Loop fetch all the keys exist in the tree */
+export function getAllKeys(treeData: DefaultOptionType[], fieldNames: InternalFieldName) {
+ const keys: React.Key[] = [];
-/**
- * Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`.
- */
-export function flattenOptions(options: any): FlattenDataNode[] {
- const typedOptions = options as InternalDataEntity[];
-
- // Add missing key
- function fillKey(list: InternalDataEntity[]): TreeDataNode[] {
- return (list || []).map(node => {
- const { value, key, children } = node;
-
- const clone: TreeDataNode = {
- ...node,
- key: 'key' in node ? key : value,
- };
+ function dig(list: DefaultOptionType[]) {
+ list.forEach(item => {
+ keys.push(item[fieldNames.value]);
+ const children = item[fieldNames.children];
if (children) {
- clone.children = fillKey(children);
+ dig(children);
}
-
- return clone;
});
}
- const flattenList = flattenTreeData(fillKey(typedOptions), true, null);
-
- const cacheMap = new Map();
- const flattenDateNodeList: (FlattenDataNode & { parentKey?: React.Key })[] = flattenList.map(
- option => {
- const { data, key, value } = option as any as Omit & {
- value: RawValueType;
- data: InternalDataEntity;
- };
-
- const flattenNode = {
- key,
- value,
- data,
- level: getLevel(option),
- parentKey: option.parent?.data.key,
- };
-
- cacheMap.set(key, flattenNode);
-
- return flattenNode;
- },
- );
-
- // Fill parent
- flattenDateNodeList.forEach(flattenNode => {
- // eslint-disable-next-line no-param-reassign
- flattenNode.parent = cacheMap.get(flattenNode.parentKey);
- });
-
- return flattenDateNodeList;
-}
-
-function getDefaultFilterOption(optionFilterProp: string) {
- return (searchValue: string, dataNode: LegacyDataNode) => {
- const value = dataNode[optionFilterProp];
-
- return String(value).toLowerCase().includes(String(searchValue).toLowerCase());
- };
-}
-
-/** Filter options and return a new options by the search text */
-export function filterOptions(
- searchValue: string,
- options: DataNode[],
- {
- optionFilterProp,
- filterOption,
- }: {
- optionFilterProp: string;
- filterOption: boolean | FilterFunc;
- },
-): DataNode[] {
- if (filterOption === false) {
- return options;
- }
-
- let filterOptionFunc: FilterFunc;
- if (typeof filterOption === 'function') {
- filterOptionFunc = filterOption;
- } else {
- filterOptionFunc = getDefaultFilterOption(optionFilterProp);
- }
-
- function dig(list: DataNode[], keepAll: boolean = false) {
- return list
- .map(dataNode => {
- const { children } = dataNode;
+ dig(treeData);
- const match = keepAll || filterOptionFunc(searchValue, fillLegacyProps(dataNode));
- const childList = dig(children || [], match);
-
- if (match || childList.length) {
- return {
- ...dataNode,
- children: childList,
- };
- }
- return null;
- })
- .filter(node => node);
- }
-
- return dig(options);
-}
-
-export function getRawValueLabeled(
- values: RawValueType[],
- prevValue: DefaultValueType,
- getEntityByValue: (
- value: RawValueType,
- skipType?: SkipType,
- ignoreDisabledCheck?: boolean,
- ) => FlattenDataNode,
- getLabelProp: (entity: FlattenDataNode, val: RawValueType) => React.ReactNode,
-): LabelValueType[] {
- const valueMap = new Map();
-
- toArray(prevValue).forEach(item => {
- if (item && typeof item === 'object' && 'value' in item) {
- valueMap.set(item.value, item);
- }
- });
-
- return values.map(val => {
- const item: LabelValueType = { value: val };
- const entity = getEntityByValue(val, 'select', true);
-
- // Always try to get the value by entity even it's enpty
- let label = getLabelProp(entity, val);
- if (label === undefined) {
- label = val;
- }
-
- if (valueMap.has(val)) {
- const labeledValue = valueMap.get(val);
- item.label = 'label' in labeledValue ? labeledValue.label : label;
- if ('halfChecked' in labeledValue) {
- item.halfChecked = labeledValue.halfChecked;
- }
- } else {
- item.label = label;
- }
-
- return item;
- });
+ return keys;
}
-export function addValue(rawValues: RawValueType[], value: RawValueType) {
- const values = new Set(rawValues);
- values.add(value);
- return Array.from(values);
-}
-export function removeValue(rawValues: RawValueType[], value: RawValueType) {
- const values = new Set(rawValues);
- values.delete(value);
- return Array.from(values);
+export function isNil(val: any) {
+ return val === null || val === undefined;
}
diff --git a/src/utils/warningPropsUtil.ts b/src/utils/warningPropsUtil.ts
index 0078ec10..9743b946 100644
--- a/src/utils/warningPropsUtil.ts
+++ b/src/utils/warningPropsUtil.ts
@@ -2,7 +2,7 @@ import warning from 'rc-util/lib/warning';
import type { TreeSelectProps } from '../TreeSelect';
import { toArray } from './valueUtil';
-function warningProps(props: TreeSelectProps) {
+function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) {
const {
searchPlaceholder,
treeCheckStrictly,
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
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" />
禁用
);
diff --git a/tests/Select.internal.spec.tsx b/tests/Select.internal.spec.tsx
deleted file mode 100644
index 5180eff3..00000000
--- a/tests/Select.internal.spec.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import TreeSelect from '../src';
-
-describe('TreeSelect.InternalAPI', () => {
- it('labelRender', () => {
- const wrapper = mount(
-
{
- let current = entity;
- const nodes = [];
-
- while (current) {
- nodes.unshift(current.data.node.label);
- current = current.parent;
- }
-
- return nodes.join('>');
- }}
- />,
- );
- expect(wrapper.find('.rc-tree-select-selection-item').text()).toEqual('Bamboo>Light>Little');
- });
-});
diff --git a/tests/Select.multiple.spec.js b/tests/Select.multiple.spec.js
index 2fb38350..93012bd2 100644
--- a/tests/Select.multiple.spec.js
+++ b/tests/Select.multiple.spec.js
@@ -262,4 +262,21 @@ describe('TreeSelect.multiple', () => {
expect(wrapper.render()).toMatchSnapshot();
});
+
+ it('not exist value should can be remove', () => {
+ const onChange = jest.fn();
+ const onDeselect = jest.fn();
+ const wrapper = mount(
+ ,
+ );
+ wrapper.clearSelection(0);
+
+ expect(onChange).toHaveBeenCalledWith([], expect.anything(), expect.anything());
+ expect(onDeselect).toHaveBeenCalledWith('not-exist', undefined);
+ });
});
diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js
index 724cfc14..6ba15fd8 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;
}
@@ -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({
diff --git a/tests/Select.spec.js b/tests/Select.spec.js
index 128e1299..5fce19aa 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';
@@ -327,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', () => {
@@ -355,7 +354,7 @@ describe('TreeSelect.basic', () => {
wrapper.openSelect();
wrapper.selectNode();
wrapper.clearAll();
- expect(wrapper.find('Select').props().value).toHaveLength(0);
+ expect(wrapper.find('BaseSelect').prop('displayValues')).toHaveLength(0);
});
});
diff --git a/tests/Select.tree.spec.js b/tests/Select.tree.spec.js
index a1151277..ec89d8f3 100644
--- a/tests/Select.tree.spec.js
+++ b/tests/Select.tree.spec.js
@@ -146,23 +146,4 @@ describe('TreeSelect.tree', () => {
expect(wrapper.exists('.bamboo-light')).toBeTruthy();
});
-
- // https://github.com/ant-design/ant-design/issues/32806
- it('OptionList should get empty children instead of list', () => {
- const wrapper = mount(
- ,
- );
-
- const options = wrapper.find('OptionList').prop('options');
- expect(options[0].children).toBeFalsy();
- });
});
diff --git a/tests/__snapshots__/Select.checkable.spec.js.snap b/tests/__snapshots__/Select.checkable.spec.js.snap
index 043055b2..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;"
>
+