diff --git a/package.json b/package.json index bd2cd305..2320dadf 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@babel/runtime": "^7.25.7", "classnames": "2.x", "rc-select": "~14.15.2", - "rc-tree": "~5.10.0", + "rc-tree": "~5.10.1", "rc-util": "^5.43.0" }, "devDependencies": { diff --git a/src/LegacyContext.tsx b/src/LegacyContext.tsx index c90ada70..36a9bc00 100644 --- a/src/LegacyContext.tsx +++ b/src/LegacyContext.tsx @@ -1,24 +1,24 @@ import * as React from 'react'; import type { DataEntity, IconType } from 'rc-tree/lib/interface'; -import type { LegacyDataNode, SafeKey } from './interface'; +import type { LegacyDataNode, SafeKey, Key } from './interface'; interface LegacyContextProps { checkable: boolean | React.ReactNode; - checkedKeys: SafeKey[]; - halfCheckedKeys: SafeKey[]; - treeExpandedKeys: SafeKey[]; - treeDefaultExpandedKeys: SafeKey[]; - onTreeExpand: (keys: SafeKey[]) => void; + checkedKeys: Key[]; + halfCheckedKeys: Key[]; + treeExpandedKeys: Key[]; + treeDefaultExpandedKeys: Key[]; + onTreeExpand: (keys: Key[]) => void; treeDefaultExpandAll: boolean; treeIcon: IconType; showTreeIcon: boolean; switcherIcon: IconType; treeLine: boolean; treeNodeFilterProp: string; - treeLoadedKeys: SafeKey[]; + treeLoadedKeys: Key[]; treeMotion: any; loadData: (treeNode: LegacyDataNode) => Promise; - onTreeLoad: (loadedKeys: SafeKey[]) => void; + onTreeLoad: (loadedKeys: Key[]) => void; keyEntities: Record>; } diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 4e87be23..cb53c58e 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -8,7 +8,7 @@ import useMemo from 'rc-util/lib/hooks/useMemo'; import * as React from 'react'; import LegacyContext from './LegacyContext'; import TreeSelectContext from './TreeSelectContext'; -import type { SafeKey, TreeDataNode } from './interface'; +import type { Key, SafeKey } from './interface'; import { getAllKeys, isCheckDisabled } from './utils/valueUtil'; const HIDDEN_STYLE = { @@ -23,7 +23,7 @@ const HIDDEN_STYLE = { }; interface TreeEventInfo { - node: { key: SafeKey }; + node: { key: Key }; selected?: boolean; checked?: boolean; } @@ -77,8 +77,8 @@ const OptionList: React.ForwardRefRenderFunction = (_, ); // ========================== Active ========================== - const [activeKey, setActiveKey] = React.useState(null); - const activeEntity = keyEntities[activeKey]; + const [activeKey, setActiveKey] = React.useState(null); + const activeEntity = keyEntities[activeKey as SafeKey]; // ========================== Values ========================== const mergedCheckedKeys = React.useMemo(() => { @@ -112,8 +112,8 @@ const OptionList: React.ForwardRefRenderFunction = (_, }; // =========================== Keys =========================== - const [expandedKeys, setExpandedKeys] = React.useState(treeDefaultExpandedKeys); - const [searchExpandedKeys, setSearchExpandedKeys] = React.useState(null); + const [expandedKeys, setExpandedKeys] = React.useState(treeDefaultExpandedKeys); + const [searchExpandedKeys, setSearchExpandedKeys] = React.useState(null); const mergedExpandedKeys = React.useMemo(() => { if (treeExpandedKeys) { @@ -129,7 +129,7 @@ const OptionList: React.ForwardRefRenderFunction = (_, // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue]); - const onInternalExpand = (keys: SafeKey[]) => { + const onInternalExpand = (keys: Key[]) => { setExpandedKeys(keys); setSearchExpandedKeys(keys); @@ -143,7 +143,7 @@ const OptionList: React.ForwardRefRenderFunction = (_, event.preventDefault(); }; - const onInternalSelect = (__: SafeKey[], info: TreeEventInfo) => { + const onInternalSelect = (__: Key[], info: TreeEventInfo) => { const { node } = info; if (checkable && isCheckDisabled(node)) { @@ -227,7 +227,7 @@ const OptionList: React.ForwardRefRenderFunction = (_, ref={treeRef} focusable={false} prefixCls={`${prefixCls}-tree`} - treeData={memoTreeData as TreeDataNode[]} + treeData={memoTreeData} height={listHeight} itemHeight={listItemHeight} itemScrollOffset={listItemScrollOffset} diff --git a/src/TreeNode.tsx b/src/TreeNode.tsx index 3afbb596..53f30341 100644 --- a/src/TreeNode.tsx +++ b/src/TreeNode.tsx @@ -1,9 +1,9 @@ /* istanbul ignore file */ import type * as React from 'react'; -import type { DataNode, SafeKey } from './interface'; +import type { DataNode, Key } from './interface'; export interface TreeNodeProps extends Omit { - value: SafeKey; + value: Key; children?: React.ReactNode; } diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx index 7551bd92..e8046954 100644 --- a/src/TreeSelect.tsx +++ b/src/TreeSelect.tsx @@ -1,9 +1,4 @@ -import type { - BaseSelectProps, - BaseSelectPropsWithoutPrivate, - BaseSelectRef, - SelectProps, -} from 'rc-select'; +import type { BaseSelectPropsWithoutPrivate, BaseSelectRef } from 'rc-select'; import { BaseSelect } from 'rc-select'; import useId from 'rc-select/lib/hooks/useId'; import type { IconType } from 'rc-tree/lib/interface'; @@ -28,72 +23,21 @@ import type { CheckedStrategy } from './utils/strategyUtil'; import { formatStrategyValues, SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; import { fillFieldNames, isNil, toArray } from './utils/valueUtil'; import warningProps from './utils/warningPropsUtil'; -import type { LabeledValueType, SafeKey, SelectSource, DefaultValueType } from './interface'; - -export type OnInternalSelect = (value: SafeKey, info: { selected: boolean }) => void; - -/** @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: SafeKey; - /** @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?: SafeKey; - pId?: SafeKey; - rootPId?: SafeKey; -} - -export interface BaseOptionType { - disabled?: boolean; - checkable?: boolean; - disableCheckbox?: boolean; - children?: BaseOptionType[]; - [name: string]: any; -} - -export interface DefaultOptionType extends BaseOptionType { - value?: SafeKey; - title?: React.ReactNode | ((data: DefaultOptionType) => React.ReactNode); - label?: React.ReactNode; - key?: SafeKey; - children?: DefaultOptionType[]; -} - -export interface LegacyDataNode extends DefaultOptionType { - props: any; -} -export interface TreeSelectProps< - ValueType = any, - OptionType extends BaseOptionType = DefaultOptionType, -> extends Omit { +import type { + LabeledValueType, + SafeKey, + Key, + DataNode, + SimpleModeConfig, + ChangeEventExtra, + SelectSource, + DefaultValueType, + FieldNames, + LegacyDataNode, +} from './interface'; + +export interface TreeSelectProps + extends Omit { prefixCls?: string; id?: string; children?: React.ReactNode; @@ -109,12 +53,12 @@ export interface TreeSelectProps< inputValue?: string; onSearch?: (value: string) => void; autoClearSearchValue?: boolean; - filterTreeNode?: boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean); + filterTreeNode?: boolean | ((inputValue: string, treeNode: DataNode) => boolean); treeNodeFilterProp?: string; // >>> Select - onSelect?: SelectProps['onSelect']; - onDeselect?: SelectProps['onDeselect']; + onSelect?: (value: ValueType, option: OptionType) => void; + onDeselect?: (value: ValueType, option: OptionType) => void; // >>> Selector showCheckedStrategy?: CheckedStrategy; @@ -255,7 +199,7 @@ const TreeSelect = React.forwardRef((props, ref) } // ========================= FieldNames ========================= - const mergedFieldNames: InternalFieldName = React.useMemo( + const mergedFieldNames: FieldNames = React.useMemo( () => fillFieldNames(fieldNames), /* eslint-disable react-hooks/exhaustive-deps */ [JSON.stringify(fieldNames)], @@ -268,7 +212,7 @@ const TreeSelect = React.forwardRef((props, ref) postState: search => search || '', }); - const onInternalSearch: BaseSelectProps['onSearch'] = searchText => { + const onInternalSearch = searchText => { setSearchValue(searchText); onSearch?.(searchText); }; @@ -310,7 +254,7 @@ const TreeSelect = React.forwardRef((props, ref) // =========================== Label ============================ const getLabel = React.useCallback( - (item: DefaultOptionType) => { + (item: DataNode) => { if (item) { if (treeNodeLabelProp) { return item[treeNodeLabelProp]; @@ -418,7 +362,7 @@ const TreeSelect = React.forwardRef((props, ref) const displayValues = React.useMemo(() => { // Collect keys which need to show const displayKeys = formatStrategyValues( - rawCheckedValues, + rawCheckedValues as SafeKey[], mergedShowCheckedStrategy, keyEntities, mergedFieldNames, @@ -574,7 +518,7 @@ const TreeSelect = React.forwardRef((props, ref) const keyList = existRawValues.map(val => valueEntities.get(val).key); // Conduction by selected or not - let checkedKeys: SafeKey[]; + let checkedKeys: Key[]; if (selected) { ({ checkedKeys } = conductCheck(keyList, true, keyEntities)); } else { @@ -588,7 +532,7 @@ const TreeSelect = React.forwardRef((props, ref) // Fill back of keys newRawValues = [ ...missingRawValues, - ...checkedKeys.map(key => keyEntities[key].node[mergedFieldNames.value]), + ...checkedKeys.map(key => keyEntities[key as SafeKey].node[mergedFieldNames.value]), ]; } triggerChange(newRawValues, { selected, triggerValue: selectedValue }, source || 'option'); @@ -637,21 +581,19 @@ const TreeSelect = React.forwardRef((props, ref) ); // ====================== Display Change ======================== - const onDisplayValuesChange = useRefFunc( - (newValues, info) => { - const newRawValues = newValues.map(item => item.value); + const onDisplayValuesChange = useRefFunc((newValues, info) => { + const newRawValues = newValues.map(item => item.value); - if (info.type === 'clear') { - triggerChange(newRawValues, {}, 'selection'); - return; - } + 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' }); - } - }, - ); + // 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( @@ -760,7 +702,7 @@ if (process.env.NODE_ENV !== 'production') { const GenericTreeSelect = TreeSelect as unknown as (< ValueType = any, - OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType, + OptionType extends DataNode = DataNode, >( props: React.PropsWithChildren> & { ref?: React.Ref; diff --git a/src/TreeSelectContext.ts b/src/TreeSelectContext.ts index 628d3742..b0aff525 100644 --- a/src/TreeSelectContext.ts +++ b/src/TreeSelectContext.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import type { ExpandAction } from 'rc-tree/lib/Tree'; -import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect'; +import type { DataNode, FieldNames, Key } from './interface'; export interface TreeSelectContextProps { virtual?: boolean; @@ -8,9 +8,9 @@ export interface TreeSelectContextProps { listHeight: number; listItemHeight: number; listItemScrollOffset?: number; - treeData: DefaultOptionType[]; - fieldNames: InternalFieldName; - onSelect: OnInternalSelect; + treeData: DataNode[]; + fieldNames: FieldNames; + onSelect: (value: Key, info: { selected: boolean }) => void; treeExpandAction?: ExpandAction; treeTitleRender?: (node: any) => React.ReactNode; onPopupScroll?: React.UIEventHandler; diff --git a/src/hooks/useCheckedKeys.ts b/src/hooks/useCheckedKeys.ts index 64a55ceb..f70567ab 100644 --- a/src/hooks/useCheckedKeys.ts +++ b/src/hooks/useCheckedKeys.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { DataEntity } from 'rc-tree/lib/interface'; import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; -import type { LabeledValueType, SafeKey } from '../interface'; +import type { LabeledValueType, SafeKey, Key } from '../interface'; const useCheckedKeys = ( rawLabeledValues: LabeledValueType[], @@ -10,13 +10,12 @@ const useCheckedKeys = ( keyEntities: Record, ) => { return React.useMemo(() => { - const extractValues = (values: LabeledValueType[]): SafeKey[] => - values.map(({ value }) => value); + const extractValues = (values: LabeledValueType[]): Key[] => values.map(({ value }) => value); const checkedKeys = extractValues(rawLabeledValues); const halfCheckedKeys = extractValues(rawHalfCheckedValues); - const missingValues = checkedKeys.filter(key => !keyEntities[key]); + const missingValues = checkedKeys.filter(key => !keyEntities[key as SafeKey]); let finalCheckedKeys = checkedKeys; let finalHalfCheckedKeys = halfCheckedKeys; diff --git a/src/hooks/useDataEntities.ts b/src/hooks/useDataEntities.ts index 8fe48bc8..95e082d0 100644 --- a/src/hooks/useDataEntities.ts +++ b/src/hooks/useDataEntities.ts @@ -1,8 +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 { SafeKey } from '../interface'; +import type { SafeKey, FieldNames } from '../interface'; import warning from 'rc-util/lib/warning'; import { isNil } from '../utils/valueUtil'; diff --git a/src/hooks/useFilterTreeData.ts b/src/hooks/useFilterTreeData.ts index 2f764b30..29361801 100644 --- a/src/hooks/useFilterTreeData.ts +++ b/src/hooks/useFilterTreeData.ts @@ -1,14 +1,15 @@ import * as React from 'react'; -import type { DefaultOptionType, InternalFieldName, TreeSelectProps } from '../TreeSelect'; +import type { TreeSelectProps } from '../TreeSelect'; +import type { DataNode, FieldNames } from '../interface'; import { fillLegacyProps } from '../utils/legacyUtil'; type FilterFn = NonNullable; const useFilterTreeData = ( - treeData: DefaultOptionType[], + treeData: DataNode[], searchValue: string, options: { - fieldNames: InternalFieldName; + fieldNames: FieldNames; treeNodeFilterProp: string; filterTreeNode: TreeSelectProps['filterTreeNode']; }, @@ -27,8 +28,8 @@ const useFilterTreeData = ( : (_, dataNode) => String(dataNode[treeNodeFilterProp]).toUpperCase().includes(searchValue.toUpperCase()); - const filterTreeNodes = (nodes: DefaultOptionType[], keepAll = false): DefaultOptionType[] => - nodes.reduce((filtered, node) => { + const filterTreeNodes = (nodes: DataNode[], keepAll = false): DataNode[] => + nodes.reduce((filtered, node) => { const children = node[fieldChildren]; const isMatch = keepAll || filterOptionFunc(searchValue, fillLegacyProps(node)); const filteredChildren = filterTreeNodes(children || [], isMatch); diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index 96ea50ad..58ea4b10 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -1,7 +1,6 @@ import * as React from 'react'; import type { DataNode, SimpleModeConfig } from '../interface'; import { convertChildrenToData } from '../utils/legacyUtil'; -import type { DefaultOptionType } from '../TreeSelect'; function buildTreeStructure(nodes: DataNode[], config: SimpleModeConfig): DataNode[] { const { id, pId, rootPId } = config; @@ -37,7 +36,7 @@ export default function useTreeData( treeData: DataNode[], children: React.ReactNode, simpleMode: boolean | SimpleModeConfig, -): DefaultOptionType[] { +): DataNode[] { return React.useMemo(() => { if (treeData) { if (simpleMode) { diff --git a/src/interface.ts b/src/interface.ts index be7a88c6..ccacc8cb 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,12 +1,18 @@ import type * as React from 'react'; -import type { SafeKey } from 'rc-tree/lib/interface'; +import type { SafeKey, Key, DataNode as TreeDataNode } from 'rc-tree/lib/interface'; -export type { SafeKey }; +export type { SafeKey, Key }; + +export interface DataNode extends Record, Omit { + key?: Key; + value?: SafeKey; + children?: DataNode[]; +} export type SelectSource = 'option' | 'selection' | 'input' | 'clear'; export interface LabeledValueType { - key?: SafeKey; + key?: Key; value?: SafeKey; label?: React.ReactNode; /** Only works on `treeCheckStrictly` */ @@ -15,44 +21,13 @@ export interface LabeledValueType { export type DefaultValueType = SafeKey | LabeledValueType | (SafeKey | LabeledValueType)[]; -export interface DataNode { - value?: SafeKey; - title?: React.ReactNode | ((data: DataNode) => React.ReactNode); - label?: React.ReactNode; - key?: SafeKey; - disabled?: boolean; - disableCheckbox?: boolean; - checkable?: boolean; - children?: DataNode[]; - - /** Customize data info */ - [prop: string]: any; -} - -export interface InternalDataEntity { - key: SafeKey; - value: SafeKey; - title?: React.ReactNode | ((data: InternalDataEntity) => React.ReactNode); - disableCheckbox?: boolean; - disabled?: boolean; - children?: InternalDataEntity[]; - - /** Origin DataNode */ - node: DataNode; -} - export interface LegacyDataNode extends DataNode { props: any; } -export interface TreeDataNode extends DataNode { - key: SafeKey; - children?: TreeDataNode[]; -} - export interface FlattenDataNode { - data: InternalDataEntity; - key: SafeKey; + data: DataNode; + key: Key; value: SafeKey; level: number; parent?: FlattenDataNode; @@ -91,4 +66,5 @@ export interface FieldNames { value?: string; label?: string; children?: string; + _title?: string[]; } diff --git a/src/utils/legacyUtil.tsx b/src/utils/legacyUtil.tsx index 62342f56..b670f9a6 100644 --- a/src/utils/legacyUtil.tsx +++ b/src/utils/legacyUtil.tsx @@ -1,9 +1,14 @@ import * as React from 'react'; import toArray from 'rc-util/lib/Children/toArray'; import warning from 'rc-util/lib/warning'; -import type { DataNode, ChangeEventExtra, SafeKey, LegacyCheckedNode } from '../interface'; +import type { + DataNode, + ChangeEventExtra, + SafeKey, + LegacyCheckedNode, + FieldNames, +} from '../interface'; import TreeNode from '../TreeNode'; -import type { DefaultOptionType, FieldNames } from '../TreeSelect'; export function convertChildrenToData(nodes: React.ReactNode): DataNode[] { return toArray(nodes) @@ -59,7 +64,7 @@ export function fillAdditionalInfo( extra: ChangeEventExtra, triggerValue: SafeKey, checkedValues: SafeKey[], - treeData: DefaultOptionType[], + treeData: DataNode[], showPosition: boolean, fieldNames: FieldNames, ) { @@ -67,7 +72,7 @@ export function fillAdditionalInfo( let nodeList: LegacyCheckedNode[] = null; function generateMap() { - function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) { + function dig(list: DataNode[], level = '0', parentIncluded = false) { return list .map((option, index) => { const pos = `${level}-${index}`; @@ -75,7 +80,7 @@ export function fillAdditionalInfo( const included = checkedValues.includes(value); const children = dig(option[fieldNames.children] || [], pos, included); const node = ( - )}> + )}> {children.map(child => child.node)} ); diff --git a/src/utils/strategyUtil.ts b/src/utils/strategyUtil.ts index bf1b3523..747977f0 100644 --- a/src/utils/strategyUtil.ts +++ b/src/utils/strategyUtil.ts @@ -1,6 +1,5 @@ -import type { InternalFieldName } from '../TreeSelect'; import type { DataEntity } from 'rc-tree/lib/interface'; -import type { SafeKey } from '../interface'; +import type { SafeKey, FieldNames } from '../interface'; import { isCheckDisabled } from './valueUtil'; export const SHOW_ALL = 'SHOW_ALL'; @@ -13,7 +12,7 @@ export function formatStrategyValues( values: SafeKey[], strategy: CheckedStrategy, keyEntities: Record, - fieldNames: InternalFieldName, + fieldNames: FieldNames, ): SafeKey[] { const valueSet = new Set(values); @@ -34,7 +33,7 @@ export function formatStrategyValues( return values.filter(key => { const entity = keyEntities[key]; const parent = entity ? entity.parent : null; - return !parent || isCheckDisabled(parent.node) || !valueSet.has(parent.key); + return !parent || isCheckDisabled(parent.node) || !valueSet.has(parent.key as SafeKey); }); } return values; diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 047e81e1..caa44727 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -1,5 +1,4 @@ import type { DataNode, FieldNames, SafeKey } from '../interface'; -import type { DefaultOptionType, InternalFieldName } from '../TreeSelect'; export const toArray = (value: T | T[]): T[] => Array.isArray(value) ? value : value !== undefined ? [value] : []; @@ -17,13 +16,10 @@ export const fillFieldNames = (fieldNames?: FieldNames) => { export const isCheckDisabled = (node: DataNode): boolean => !node || node.disabled || node.disableCheckbox || node.checkable === false; -export const getAllKeys = ( - treeData: DefaultOptionType[], - fieldNames: InternalFieldName, -): SafeKey[] => { +export const getAllKeys = (treeData: DataNode[], fieldNames: FieldNames): SafeKey[] => { const keys: SafeKey[] = []; - const dig = (list: DefaultOptionType[]): void => { + const dig = (list: DataNode[]): void => { list.forEach(item => { const children = item[fieldNames.children]; if (children) {