diff --git a/components/style.ts b/components/style.ts index da26516a5..2cc543420 100644 --- a/components/style.ts +++ b/components/style.ts @@ -42,7 +42,7 @@ import './cascader/style'; // import './timeline/style'; // import './input-number/style'; // import './transfer/style'; -import './tree/style'; +// import './tree/style'; // import './upload/style'; // import './layout/style'; // import './anchor/style'; diff --git a/components/tree/DirectoryTree.tsx b/components/tree/DirectoryTree.tsx index d97c4ce27..6efe877b6 100644 --- a/components/tree/DirectoryTree.tsx +++ b/components/tree/DirectoryTree.tsx @@ -1,4 +1,4 @@ -import type { ExtractPropTypes, PropType } from 'vue'; +import type { ExtractPropTypes } from 'vue'; import { nextTick, onUpdated, ref, watch, defineComponent, computed } from 'vue'; import debounce from 'lodash-es/debounce'; import FolderOpenOutlined from '@ant-design/icons-vue/FolderOpenOutlined'; @@ -18,12 +18,13 @@ import { conductExpandParent } from '../vc-tree/util'; import { calcRangeKeys, convertDirectoryKeysToNodes } from './utils/dictUtil'; import useConfigInject from '../config-provider/hooks/useConfigInject'; import { filterEmpty } from '../_util/props-util'; +import { someType } from '../_util/type'; export type ExpandAction = false | 'click' | 'doubleclick' | 'dblclick'; export const directoryTreeProps = () => ({ ...treeProps(), - expandAction: { type: [Boolean, String] as PropType }, + expandAction: someType([Boolean, String]), }); export type DirectoryTreeProps = Partial>>; diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx index a023f3961..7baf7a84a 100644 --- a/components/tree/Tree.tsx +++ b/components/tree/Tree.tsx @@ -1,4 +1,4 @@ -import type { PropType, ExtractPropTypes } from 'vue'; +import type { ExtractPropTypes } from 'vue'; import { watchEffect, ref, defineComponent, computed } from 'vue'; import classNames from '../_util/classNames'; import VcTree from '../vc-tree'; @@ -15,6 +15,10 @@ import dropIndicatorRender from './utils/dropIndicator'; import devWarning from '../vc-util/devWarning'; import { warning } from '../vc-util/warning'; import omit from '../_util/omit'; +import { booleanType, someType, arrayType, functionType, objectType } from '../_util/type'; + +// CSSINJS +import useStyle from './style'; export interface AntdTreeNodeAttribute { eventKey: string; @@ -84,44 +88,39 @@ export const treeProps = () => { const baseTreeProps = vcTreeProps(); return { ...baseTreeProps, - showLine: { - type: [Boolean, Object] as PropType, - default: undefined, - }, + showLine: someType([Boolean, Object]), /** 是否支持多选 */ - multiple: { type: Boolean, default: undefined }, + multiple: booleanType(), /** 是否自动展开父节点 */ - autoExpandParent: { type: Boolean, default: undefined }, + autoExpandParent: booleanType(), /** checkable状态下节点选择完全受控(父子节点选中状态不再关联)*/ - checkStrictly: { type: Boolean, default: undefined }, + checkStrictly: booleanType(), /** 是否支持选中 */ - checkable: { type: Boolean, default: undefined }, + checkable: booleanType(), /** 是否禁用树 */ - disabled: { type: Boolean, default: undefined }, + disabled: booleanType(), /** 默认展开所有树节点 */ - defaultExpandAll: { type: Boolean, default: undefined }, + defaultExpandAll: booleanType(), /** 默认展开对应树节点 */ - defaultExpandParent: { type: Boolean, default: undefined }, + defaultExpandParent: booleanType(), /** 默认展开指定的树节点 */ - defaultExpandedKeys: { type: Array as PropType }, + defaultExpandedKeys: arrayType(), /** (受控)展开指定的树节点 */ - expandedKeys: { type: Array as PropType }, + expandedKeys: arrayType(), /** (受控)选中复选框的树节点 */ - checkedKeys: { - type: [Array, Object] as PropType, - }, + checkedKeys: someType([Array, Object]), /** 默认选中复选框的树节点 */ - defaultCheckedKeys: { type: Array as PropType }, + defaultCheckedKeys: arrayType(), /** (受控)设置选中的树节点 */ - selectedKeys: { type: Array as PropType }, + selectedKeys: arrayType(), /** 默认选中的树节点 */ - defaultSelectedKeys: { type: Array as PropType }, - selectable: { type: Boolean, default: undefined }, + defaultSelectedKeys: arrayType(), + selectable: booleanType(), - loadedKeys: { type: Array as PropType }, - draggable: { type: Boolean, default: undefined }, - showIcon: { type: Boolean, default: undefined }, - icon: { type: Function as PropType<(nodeProps: AntdTreeNodeAttribute) => any> }, + loadedKeys: arrayType(), + draggable: booleanType(), + showIcon: booleanType(), + icon: functionType<(nodeProps: AntdTreeNodeAttribute) => any>(), switcherIcon: PropTypes.any, prefixCls: String, /** @@ -129,13 +128,13 @@ export const treeProps = () => { * deprecated, please use `fieldNames` instead * 替换treeNode中 title,key,children字段为treeData中对应的字段 */ - replaceFields: { type: Object as PropType }, - blockNode: { type: Boolean, default: undefined }, + replaceFields: objectType(), + blockNode: booleanType(), openAnimation: PropTypes.any, onDoubleclick: baseTreeProps.onDblclick, - 'onUpdate:selectedKeys': Function as PropType<(keys: Key[]) => void>, - 'onUpdate:checkedKeys': Function as PropType<(keys: Key[]) => void>, - 'onUpdate:expandedKeys': Function as PropType<(keys: Key[]) => void>, + 'onUpdate:selectedKeys': functionType<(keys: Key[]) => void>(), + 'onUpdate:checkedKeys': functionType<(keys: Key[]) => void>(), + 'onUpdate:expandedKeys': functionType<(keys: Key[]) => void>(), }; }; @@ -168,6 +167,10 @@ export default defineComponent({ '`children` of Tree is deprecated. Please use `treeData` instead.', ); const { prefixCls, direction, virtual } = useConfigInject('tree', props); + + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const treeRef = ref(); const scrollTo: ScrollTo = scroll => { treeRef.value?.scrollTo(scroll); @@ -236,7 +239,7 @@ export default defineComponent({ itemHeight, }; const children = slots.default ? filterEmpty(slots.default()) : undefined; - return ( + return wrapSSR( , }} children={children} - > + >, ); }; }, diff --git a/components/tree/index.en-US.md b/components/tree/index.en-US.md index 9cfec4d1d..59f20ba03 100644 --- a/components/tree/index.en-US.md +++ b/components/tree/index.en-US.md @@ -2,7 +2,7 @@ category: Components type: Data Display title: Tree -cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Ag9_Q6ArswEAAAAAAAAAAAAADrJ8AQ/original --- A hierarchical list structure component. @@ -17,6 +17,7 @@ Almost anything can be represented in a tree structure. Examples include directo | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | +| allowDrop | Whether to allow dropping on the node | ({ dropNode, dropPosition }) => boolean | - | | | autoExpandParent | Whether to automatically expand a parent treeNode | boolean | false | | | blockNode | Whether treeNode fill remaining horizontal space | boolean | false | | | checkable | Adds a `Checkbox` before the treeNodes | boolean | false | | @@ -28,6 +29,7 @@ Almost anything can be represented in a tree structure. Examples include directo | expandedKeys(v-model) | (Controlled) Specifies the keys of the expanded treeNodes | string\[] \| number\[] | \[] | | | fieldNames | Replace the title,key and children fields in treeNode with the corresponding fields in treeData | object | { children:'children', title:'title', key:'key' } | 3.0.0 | | filterTreeNode | Defines a function to filter (highlight) treeNodes. When the function returns `true`, the corresponding treeNode will be highlighted | function(node) | - | | +| height | Config virtual scroll height. Will not support horizontal scroll when enable this | number | - | | | loadData | Load data asynchronously | function(node) | - | | | loadedKeys | (Controlled) Set loaded tree nodes. Need work with `loadData` | string\[] \| number\[] | \[] | | | multiple | Allows selecting multiple treeNodes | boolean | false | | diff --git a/components/tree/index.zh-CN.md b/components/tree/index.zh-CN.md index 0e58afa34..b3aace369 100644 --- a/components/tree/index.zh-CN.md +++ b/components/tree/index.zh-CN.md @@ -3,7 +3,7 @@ category: Components type: 数据展示 title: Tree subtitle: 树形控件 -cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Ag9_Q6ArswEAAAAAAAAAAAAADrJ8AQ/original --- 多层次的结构列表。 @@ -18,6 +18,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg | 参数 | 说明 | 类型 | 默认值 | 版本 | | | --- | --- | --- | --- | --- | --- | +| allowDrop | 是否允许拖拽时放置在该节点 | ({ dropNode, dropPosition }) => boolean | - | | | autoExpandParent | 是否自动展开父节点 | boolean | false | | | | blockNode | 是否节点占据一行 | boolean | false | | | | checkable | 节点前添加 Checkbox 复选框 | boolean | false | | | @@ -29,6 +30,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg | expandedKeys(v-model) | (受控)展开指定的树节点 | string\[] \| number\[] | \[] | | | | fieldNames | 替换 treeNode 中 title,key,children 字段为 treeData 中对应的字段 | object | {children:'children', title:'title', key:'key' } | 3.0.0 | | | filterTreeNode | 按需筛选树节点(高亮),返回 true | function(node) | - | | | +| height | 设置虚拟滚动容器高度,设置后内部节点不再支持横向滚动 | number | - | | | loadData | 异步加载数据 | function(node) | - | | | | loadedKeys | (受控)已经加载的节点,需要配合 `loadData` 使用 | string\[] \| number\[] | \[] | | | | multiple | 支持点选多个节点(节点本身) | boolean | false | | | diff --git a/components/tree/style/index.tsx b/components/tree/style/index.tsx index 3a3ab0de5..ffc90eecd 100644 --- a/components/tree/style/index.tsx +++ b/components/tree/style/index.tsx @@ -1,2 +1,482 @@ -import '../../style/index.less'; -import './index.less'; +import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; +import { Keyframes } from '../../_util/cssinjs'; +import { genCollapseMotion } from '../../_style/motion'; +import { getStyle as getCheckboxStyle } from '../../checkbox/style'; +import type { DerivativeToken } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { genFocusOutline, resetComponent } from '../../_style'; + +// ============================ Keyframes ============================= +const treeNodeFX = new Keyframes('ant-tree-node-fx-do-not-use', { + '0%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, +}); + +// ============================== Switch ============================== +const getSwitchStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({ + [`.${prefixCls}-switcher-icon`]: { + display: 'inline-block', + fontSize: 10, + verticalAlign: 'baseline', + + svg: { + transition: `transform ${token.motionDurationSlow}`, + }, + }, +}); + +// =============================== Drop =============================== +const getDropIndicatorStyle = (prefixCls: string, token: DerivativeToken) => ({ + [`.${prefixCls}-drop-indicator`]: { + position: 'absolute', + // it should displayed over the following node + zIndex: 1, + height: 2, + backgroundColor: token.colorPrimary, + borderRadius: 1, + pointerEvents: 'none', + + '&:after': { + position: 'absolute', + top: -3, + insetInlineStart: -6, + width: 8, + height: 8, + backgroundColor: 'transparent', + border: `${token.lineWidthBold}px solid ${token.colorPrimary}`, + borderRadius: '50%', + content: '""', + }, + }, +}); + +// =============================== Base =============================== +type TreeToken = DerivativeToken & { + treeCls: string; + treeNodeCls: string; + treeNodePadding: number; + treeTitleHeight: number; +}; + +export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject => { + const { treeCls, treeNodeCls, treeNodePadding, treeTitleHeight } = token; + + const treeCheckBoxMarginVertical = (treeTitleHeight - token.fontSizeLG) / 2; + const treeCheckBoxMarginHorizontal = token.paddingXS; + + return { + [treeCls]: { + ...resetComponent(token), + background: token.colorBgContainer, + borderRadius: token.borderRadius, + transition: `background-color ${token.motionDurationSlow}`, + + [`&${treeCls}-rtl`]: { + // >>> Switcher + [`${treeCls}-switcher`]: { + '&_close': { + [`${treeCls}-switcher-icon`]: { + svg: { + transform: 'rotate(90deg)', + }, + }, + }, + }, + }, + + [`&-focused:not(:hover):not(${treeCls}-active-focused)`]: { + ...genFocusOutline(token), + }, + + // =================== Virtual List =================== + [`${treeCls}-list-holder-inner`]: { + alignItems: 'flex-start', + }, + + [`&${treeCls}-block-node`]: { + [`${treeCls}-list-holder-inner`]: { + alignItems: 'stretch', + + // >>> Title + [`${treeCls}-node-content-wrapper`]: { + flex: 'auto', + }, + + // >>> Drag + [`${treeNodeCls}.dragging`]: { + position: 'relative', + + '&:after': { + position: 'absolute', + top: 0, + insetInlineEnd: 0, + bottom: treeNodePadding, + insetInlineStart: 0, + border: `1px solid ${token.colorPrimary}`, + opacity: 0, + animationName: treeNodeFX, + animationDuration: token.motionDurationSlow, + animationPlayState: 'running', + animationFillMode: 'forwards', + content: '""', + pointerEvents: 'none', + }, + }, + }, + }, + + // ===================== TreeNode ===================== + [`${treeNodeCls}`]: { + display: 'flex', + alignItems: 'flex-start', + padding: `0 0 ${treeNodePadding}px 0`, + outline: 'none', + + '&-rtl': { + direction: 'rtl', + }, + + // Disabled + '&-disabled': { + // >>> Title + [`${treeCls}-node-content-wrapper`]: { + color: token.colorTextDisabled, + cursor: 'not-allowed', + '&:hover': { + background: 'transparent', + }, + }, + }, + + [`&-active ${treeCls}-node-content-wrapper`]: { + ...genFocusOutline(token), + }, + + [`&:not(${treeNodeCls}-disabled).filter-node ${treeCls}-title`]: { + color: 'inherit', + fontWeight: 500, + }, + + '&-draggable': { + [`${treeCls}-draggable-icon`]: { + width: treeTitleHeight, + lineHeight: `${treeTitleHeight}px`, + textAlign: 'center', + visibility: 'visible', + opacity: 0.2, + transition: `opacity ${token.motionDurationSlow}`, + + [`${treeNodeCls}:hover &`]: { + opacity: 0.45, + }, + }, + + [`&${treeNodeCls}-disabled`]: { + [`${treeCls}-draggable-icon`]: { + visibility: 'hidden', + }, + }, + }, + }, + + // >>> Indent + [`${treeCls}-indent`]: { + alignSelf: 'stretch', + whiteSpace: 'nowrap', + userSelect: 'none', + '&-unit': { + display: 'inline-block', + width: treeTitleHeight, + }, + }, + + // >>> Drag Handler + [`${treeCls}-draggable-icon`]: { + visibility: 'hidden', + }, + + // >>> Switcher + [`${treeCls}-switcher`]: { + ...getSwitchStyle(prefixCls, token), + position: 'relative', + flex: 'none', + alignSelf: 'stretch', + width: treeTitleHeight, + margin: 0, + lineHeight: `${treeTitleHeight}px`, + textAlign: 'center', + cursor: 'pointer', + userSelect: 'none', + + '&-noop': { + cursor: 'default', + }, + + '&_close': { + [`${treeCls}-switcher-icon`]: { + svg: { + transform: 'rotate(-90deg)', + }, + }, + }, + + '&-loading-icon': { + color: token.colorPrimary, + }, + + '&-leaf-line': { + position: 'relative', + zIndex: 1, + display: 'inline-block', + width: '100%', + height: '100%', + + // https://github.com/ant-design/ant-design/issues/31884 + '&:before': { + position: 'absolute', + top: 0, + insetInlineEnd: treeTitleHeight / 2, + bottom: -treeNodePadding, + marginInlineStart: -1, + borderInlineEnd: `1px solid ${token.colorBorder}`, + content: '""', + }, + + '&:after': { + position: 'absolute', + width: (treeTitleHeight / 2) * 0.8, + height: treeTitleHeight / 2, + borderBottom: `1px solid ${token.colorBorder}`, + content: '""', + }, + }, + }, + + // >>> Checkbox + [`${treeCls}-checkbox`]: { + top: 'initial', + marginInlineEnd: treeCheckBoxMarginHorizontal, + marginBlockStart: treeCheckBoxMarginVertical, + }, + + // >>> Title + // add `${treeCls}-checkbox + span` to cover checkbox `${checkboxCls} + span` + [`${treeCls}-node-content-wrapper, ${treeCls}-checkbox + span`]: { + position: 'relative', + zIndex: 'auto', + minHeight: treeTitleHeight, + margin: 0, + padding: `0 ${token.paddingXS / 2}px`, + color: 'inherit', + lineHeight: `${treeTitleHeight}px`, + background: 'transparent', + borderRadius: token.borderRadius, + cursor: 'pointer', + transition: `all ${token.motionDurationMid}, border 0s, line-height 0s, box-shadow 0s`, + + '&:hover': { + backgroundColor: token.controlItemBgHover, + }, + + [`&${treeCls}-node-selected`]: { + backgroundColor: token.controlItemBgActive, + }, + + // Icon + [`${treeCls}-iconEle`]: { + display: 'inline-block', + width: treeTitleHeight, + height: treeTitleHeight, + lineHeight: `${treeTitleHeight}px`, + textAlign: 'center', + verticalAlign: 'top', + + '&:empty': { + display: 'none', + }, + }, + }, + + // https://github.com/ant-design/ant-design/issues/28217 + [`${treeCls}-unselectable ${treeCls}-node-content-wrapper:hover`]: { + backgroundColor: 'transparent', + }, + + // ==================== Draggable ===================== + [`${treeCls}-node-content-wrapper`]: { + lineHeight: `${treeTitleHeight}px`, + userSelect: 'none', + + ...getDropIndicatorStyle(prefixCls, token), + }, + + [`${treeNodeCls}.drop-container`]: { + '> [draggable]': { + boxShadow: `0 0 0 2px ${token.colorPrimary}`, + }, + }, + + // ==================== Show Line ===================== + '&-show-line': { + // ================ Indent lines ================ + [`${treeCls}-indent`]: { + '&-unit': { + position: 'relative', + height: '100%', + + '&:before': { + position: 'absolute', + top: 0, + insetInlineEnd: treeTitleHeight / 2, + bottom: -treeNodePadding, + borderInlineEnd: `1px solid ${token.colorBorder}`, + content: '""', + }, + + '&-end': { + '&:before': { + display: 'none', + }, + }, + }, + }, + + // ============== Cover Background ============== + [`${treeCls}-switcher`]: { + background: 'transparent', + + '&-line-icon': { + // https://github.com/ant-design/ant-design/issues/32813 + verticalAlign: '-0.15em', + }, + }, + }, + + [`${treeNodeCls}-leaf-last`]: { + [`${treeCls}-switcher`]: { + '&-leaf-line': { + '&:before': { + top: 'auto !important', + bottom: 'auto !important', + height: `${treeTitleHeight / 2}px !important`, + }, + }, + }, + }, + }, + }; +}; + +// ============================ Directory ============================= +export const genDirectoryStyle = (token: TreeToken): CSSObject => { + const { treeCls, treeNodeCls, treeNodePadding } = token; + + return { + [`${treeCls}${treeCls}-directory`]: { + // ================== TreeNode ================== + [treeNodeCls]: { + position: 'relative', + + // Hover color + '&:before': { + position: 'absolute', + top: 0, + insetInlineEnd: 0, + bottom: treeNodePadding, + insetInlineStart: 0, + transition: `background-color ${token.motionDurationMid}`, + content: '""', + pointerEvents: 'none', + }, + + '&:hover': { + '&:before': { + background: token.controlItemBgHover, + }, + }, + + // Elements + '> *': { + zIndex: 1, + }, + + // >>> Switcher + [`${treeCls}-switcher`]: { + transition: `color ${token.motionDurationMid}`, + }, + + // >>> Title + [`${treeCls}-node-content-wrapper`]: { + borderRadius: 0, + userSelect: 'none', + + '&:hover': { + background: 'transparent', + }, + + [`&${treeCls}-node-selected`]: { + color: token.colorTextLightSolid, + background: 'transparent', + }, + }, + + // ============= Selected ============= + '&-selected': { + [` + &:hover::before, + &::before + `]: { + background: token.colorPrimary, + }, + + // >>> Switcher + [`${treeCls}-switcher`]: { + color: token.colorTextLightSolid, + }, + + // >>> Title + [`${treeCls}-node-content-wrapper`]: { + color: token.colorTextLightSolid, + background: 'transparent', + }, + }, + }, + }, + }; +}; + +// ============================== Merged ============================== +export const genTreeStyle = (prefixCls: string, token: DerivativeToken): CSSInterpolation => { + const treeCls = `.${prefixCls}`; + const treeNodeCls = `${treeCls}-treenode`; + + const treeNodePadding = token.paddingXS / 2; + const treeTitleHeight = token.controlHeightSM; + + const treeToken = mergeToken(token, { + treeCls, + treeNodeCls, + treeNodePadding, + treeTitleHeight, + }); + + return [ + // Basic + genBaseStyle(prefixCls, treeToken), + // Directory + genDirectoryStyle(treeToken), + ]; +}; + +// ============================== Export ============================== +export default genComponentStyleHook('Tree', (token, { prefixCls }) => [ + { + [token.componentCls]: getCheckboxStyle(`${prefixCls}-checkbox`, token), + }, + genTreeStyle(prefixCls, token), + genCollapseMotion(token), +]);