Files
ant-design/components/tree/Tree.tsx
lijianan 5c52fea0bf refactor(types): derive SemanticName from semantic maps (#56389)
* refactor(types): derive SemanticName from semantic maps

* update type
2025-12-27 18:12:44 +08:00

347 lines
9.4 KiB
TypeScript

import type { Component } from 'react';
import React from 'react';
import HolderOutlined from '@ant-design/icons/HolderOutlined';
import type { CSSMotionProps } from '@rc-component/motion';
import type { BasicDataNode, TreeProps as RcTreeProps } from '@rc-component/tree';
import RcTree from '@rc-component/tree';
import type { DataNode, Key } from '@rc-component/tree/lib/interface';
import { clsx } from 'clsx';
import { useMergeSemantic } from '../_util/hooks';
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks';
import initCollapseMotion from '../_util/motion';
import { ConfigContext } from '../config-provider';
import { useComponentConfig } from '../config-provider/context';
import DisabledContext from '../config-provider/DisabledContext';
import { useToken } from '../theme/internal';
import useStyle from './style';
import dropIndicatorRender from './utils/dropIndicator';
import SwitcherIconCom from './utils/iconUtil';
export type SwitcherIcon = React.ReactNode | ((props: AntTreeNodeProps) => React.ReactNode);
export type TreeLeafIcon = React.ReactNode | ((props: AntTreeNodeProps) => React.ReactNode);
type TreeIcon = React.ReactNode | ((props: AntdTreeNodeAttribute) => React.ReactNode);
export interface AntdTreeNodeAttribute {
eventKey: string;
prefixCls: string;
className: string;
expanded: boolean;
selected: boolean;
checked: boolean;
halfChecked: boolean;
children: React.ReactNode;
title: React.ReactNode;
pos: string;
dragOver: boolean;
dragOverGapTop: boolean;
dragOverGapBottom: boolean;
isLeaf: boolean;
selectable: boolean;
disabled: boolean;
disableCheckbox: boolean;
}
export interface AntTreeNodeProps {
className?: string;
checkable?: boolean;
disabled?: boolean;
disableCheckbox?: boolean;
title?: React.ReactNode | ((data: DataNode) => React.ReactNode);
key?: Key;
eventKey?: Key;
isLeaf?: boolean;
checked?: boolean;
expanded?: boolean;
loading?: boolean;
selected?: boolean;
selectable?: boolean;
icon?: TreeIcon;
children?: React.ReactNode;
[customProp: string]: any;
}
export interface AntTreeNode extends Component<AntTreeNodeProps> {}
export interface AntTreeNodeBaseEvent {
node: AntTreeNode;
nativeEvent: MouseEvent;
}
export interface AntTreeNodeCheckedEvent extends AntTreeNodeBaseEvent {
event: 'check';
checked?: boolean;
checkedNodes?: AntTreeNode[];
}
export interface AntTreeNodeSelectedEvent extends AntTreeNodeBaseEvent {
event: 'select';
selected?: boolean;
selectedNodes?: DataNode[];
}
export interface AntTreeNodeExpandedEvent extends AntTreeNodeBaseEvent {
expanded?: boolean;
}
export interface AntTreeNodeMouseEvent {
node: AntTreeNode;
event: React.DragEvent<HTMLElement>;
}
export interface AntTreeNodeDragEnterEvent extends AntTreeNodeMouseEvent {
expandedKeys: Key[];
}
export interface AntTreeNodeDropEvent {
node: AntTreeNode;
dragNode: AntTreeNode;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap?: boolean;
event: React.MouseEvent<HTMLElement>;
}
// [Legacy] Compatible for v3
export type TreeNodeNormal = DataNode;
type DraggableFn = (node: DataNode) => boolean;
interface DraggableConfig {
icon?: React.ReactNode;
nodeDraggable?: DraggableFn;
}
export type TreeSemanticName = keyof TreeSemanticClassNames & keyof TreeSemanticStyles;
export type TreeSemanticClassNames = {
root?: string;
item?: string;
itemIcon?: string;
itemTitle?: string;
};
export type TreeSemanticStyles = {
root?: React.CSSProperties;
item?: React.CSSProperties;
itemIcon?: React.CSSProperties;
itemTitle?: React.CSSProperties;
};
export type TreeClassNamesType = SemanticClassNamesType<TreeProps, TreeSemanticClassNames>;
export type TreeStylesType = SemanticStylesType<TreeProps, TreeSemanticStyles>;
export interface TreeProps<T extends BasicDataNode = DataNode>
extends Omit<
RcTreeProps<T>,
| 'prefixCls'
| 'showLine'
| 'direction'
| 'draggable'
| 'icon'
| 'switcherIcon'
| 'classNames'
| 'styles'
> {
showLine?: boolean | { showLeafIcon: boolean | TreeLeafIcon };
className?: string;
classNames?: TreeClassNamesType;
styles?: TreeStylesType;
/** Whether to support multiple selection */
multiple?: boolean;
/** Whether to automatically expand the parent node */
autoExpandParent?: boolean;
/** Node selection in Checkable state is fully controlled (the selected state of parent and child nodes is no longer associated) */
checkStrictly?: boolean;
/** Whether to support selection */
checkable?: boolean;
/** whether to disable the tree */
disabled?: boolean;
/** Expand all tree nodes by default */
defaultExpandAll?: boolean;
/** Expand the corresponding tree node by default */
defaultExpandParent?: boolean;
/** Expand the specified tree node by default */
defaultExpandedKeys?: Key[];
/** (Controlled) Expand the specified tree node */
expandedKeys?: Key[];
/** (Controlled) Tree node with checked checkbox */
checkedKeys?: Key[] | { checked: Key[]; halfChecked: Key[] };
/** Tree node with checkbox checked by default */
defaultCheckedKeys?: Key[];
/** (Controlled) Set the selected tree node */
selectedKeys?: Key[];
/** Tree node selected by default */
defaultSelectedKeys?: Key[];
selectable?: boolean;
/** Click on the tree node to trigger */
filterAntTreeNode?: (node: AntTreeNode) => boolean;
loadedKeys?: Key[];
/** Set the node to be draggable (IE>8) */
draggable?: DraggableFn | boolean | DraggableConfig;
style?: React.CSSProperties;
showIcon?: boolean;
icon?: TreeIcon;
switcherIcon?: SwitcherIcon;
switcherLoadingIcon?: React.ReactNode;
prefixCls?: string;
children?: React.ReactNode;
blockNode?: boolean;
}
const Tree = React.forwardRef<RcTree, TreeProps>((props, ref) => {
const {
getPrefixCls,
direction,
className: contextClassName,
style: contextStyle,
classNames: contextClassNames,
styles: contextStyles,
} = useComponentConfig('tree');
const { virtual } = React.useContext(ConfigContext);
const {
prefixCls: customizePrefixCls,
className,
showIcon = false,
showLine,
switcherIcon,
switcherLoadingIcon,
blockNode = false,
children,
checkable = false,
selectable = true,
draggable,
disabled,
motion: customMotion,
style,
rootClassName,
classNames,
styles,
} = props;
const contextDisabled = React.useContext(DisabledContext);
const mergedDisabled = disabled ?? contextDisabled;
const prefixCls = getPrefixCls('tree', customizePrefixCls);
const rootPrefixCls = getPrefixCls();
const motion: CSSMotionProps = customMotion ?? {
...initCollapseMotion(rootPrefixCls),
motionAppear: false,
};
// =========== Merged Props for Semantic ==========
const mergedProps: TreeProps = {
...props,
showIcon,
blockNode,
checkable,
selectable,
disabled: mergedDisabled,
motion,
};
const [mergedClassNames, mergedStyles] = useMergeSemantic<
TreeClassNamesType,
TreeStylesType,
TreeProps
>([contextClassNames, classNames], [contextStyles, styles], {
props: mergedProps,
});
const newProps = {
...props,
checkable,
selectable,
showIcon,
motion,
blockNode,
disabled: mergedDisabled,
showLine: Boolean(showLine),
dropIndicatorRender,
};
const [hashId, cssVarCls] = useStyle(prefixCls);
const [, token] = useToken();
const itemHeight = token.paddingXS / 2 + (token.Tree?.titleHeight || token.controlHeightSM);
const draggableConfig = React.useMemo(() => {
if (!draggable) {
return false;
}
let mergedDraggable: DraggableConfig = {};
switch (typeof draggable) {
case 'function':
mergedDraggable.nodeDraggable = draggable;
break;
case 'object':
mergedDraggable = { ...draggable };
break;
default:
break;
// Do nothing
}
if (mergedDraggable.icon !== false) {
mergedDraggable.icon = mergedDraggable.icon || <HolderOutlined />;
}
return mergedDraggable;
}, [draggable]);
const renderSwitcherIcon = (nodeProps: AntTreeNodeProps) => (
<SwitcherIconCom
prefixCls={prefixCls}
switcherIcon={switcherIcon}
switcherLoadingIcon={switcherLoadingIcon}
treeNodeProps={nodeProps}
showLine={showLine}
/>
);
return (
// @ts-ignore
<RcTree
itemHeight={itemHeight}
ref={ref}
virtual={virtual}
{...newProps}
// newProps may contain style so declare style below it
prefixCls={prefixCls}
className={clsx(
{
[`${prefixCls}-icon-hide`]: !showIcon,
[`${prefixCls}-block-node`]: blockNode,
[`${prefixCls}-unselectable`]: !selectable,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-disabled`]: mergedDisabled,
},
contextClassName,
className,
hashId,
cssVarCls,
)}
style={{ ...contextStyle, ...style }}
rootClassName={clsx(mergedClassNames?.root, rootClassName)}
rootStyle={mergedStyles?.root}
classNames={mergedClassNames}
styles={mergedStyles}
direction={direction}
checkable={checkable ? <span className={`${prefixCls}-checkbox-inner`} /> : checkable}
selectable={selectable}
switcherIcon={renderSwitcherIcon}
draggable={draggableConfig}
>
{children}
</RcTree>
);
});
if (process.env.NODE_ENV !== 'production') {
Tree.displayName = 'Tree';
}
export default Tree;