diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx index 6900339b29..e9854b478a 100644 --- a/components/tree/Tree.tsx +++ b/components/tree/Tree.tsx @@ -11,6 +11,7 @@ import dropIndicatorRender from './utils/dropIndicator'; import renderSwitcherIcon from './utils/iconUtil'; export type SwitcherIcon = React.ReactNode | ((props: AntTreeNodeProps) => React.ReactNode); +export type TreeLeafIcon = React.ReactNode | ((props: AntTreeNodeProps) => React.ReactNode); export interface AntdTreeNodeAttribute { eventKey: string; @@ -107,7 +108,7 @@ export interface TreeProps RcTreeProps, 'prefixCls' | 'showLine' | 'direction' | 'draggable' | 'icon' | 'switcherIcon' > { - showLine?: boolean | { showLeafIcon: boolean }; + showLine?: boolean | { showLeafIcon: boolean | TreeLeafIcon }; className?: string; /** 是否支持多选 */ multiple?: boolean; diff --git a/components/tree/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/tree/__tests__/__snapshots__/demo-extend.test.ts.snap index a81be15fef..b24238c819 100644 --- a/components/tree/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/tree/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1765,24 +1765,8 @@ Array [ class="ant-tree-switcher ant-tree-switcher-noop" > - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + />
showLeafIcon: - + aria-hidden="true" + class="ant-select-arrow" + style="user-select:none;-webkit-user-select:none" + unselectable="on" + > + + + +
+
- - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + />
showLeafIcon: - + aria-hidden="true" + class="ant-select-arrow" + style="user-select:none;-webkit-user-select:none" + unselectable="on" + > + + + +
+
- - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> - - + class="ant-tree-switcher-leaf-line" + /> { const { container } = render( } defaultExpandAll> - - + + , ); expect(container.querySelectorAll('.switcherIcon').length).toBe(1); }); + it('leaf nodes should render custom icons when provided', () => { + const { container } = render( + }} defaultExpandAll> + + + + + , + ); + expect(container.querySelectorAll('.customLeafIcon').length).toBe(2); + }); + + it('leaf nodes should render custom icons when provided as render function', () => { + const { container } = render( + }} defaultExpandAll> + + + + + , + ); + + expect(container.querySelectorAll('.customLeafIcon').length).toBe(2); + }); + + it('leaf nodes should render custom icons when provided as string', async () => { + render( + + + + + + , + ); + + const customIcons = await screen.findAllByText('customLeafIcon'); + expect(customIcons).toHaveLength(2); + }); + it('switcherIcon in Tree could be string', () => { const { asFragment } = render( - - + + , ); @@ -66,8 +105,8 @@ describe('Tree', () => { const { asFragment } = render( - - + + , ); @@ -83,8 +122,8 @@ describe('Tree', () => { } > - - + + , ); diff --git a/components/tree/__tests__/util.test.ts b/components/tree/__tests__/util.test.ts deleted file mode 100644 index 053f7a2ed6..0000000000 --- a/components/tree/__tests__/util.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { calcRangeKeys } from '../utils/dictUtil'; - -describe('Tree util', () => { - describe('calcRangeKeys', () => { - const treeData = [ - { key: '0-0', children: [{ key: '0-0-0' }, { key: '0-0-1' }] }, - { key: '0-1', children: [{ key: '0-1-0' }, { key: '0-1-1' }] }, - { - key: '0-2', - children: [ - { key: '0-2-0', children: [{ key: '0-2-0-0' }, { key: '0-2-0-1' }, { key: '0-2-0-2' }] }, - ], - }, - ]; - - it('calc range keys', () => { - const rangeKeys = calcRangeKeys({ - treeData, - expandedKeys: ['0-0', '0-2', '0-2-0'], - startKey: '0-2-0-1', - endKey: '0-0-0', - }); - const target = ['0-0-0', '0-0-1', '0-1', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1']; - expect(rangeKeys.sort()).toEqual(target.sort()); - }); - - it('return startKey when startKey === endKey', () => { - const keys = calcRangeKeys({ - treeData, - expandedKeys: ['0-0', '0-2', '0-2-0'], - startKey: '0-0-0', - endKey: '0-0-0', - }); - expect(keys).toEqual(['0-0-0']); - }); - - it('return empty array without startKey and endKey', () => { - const keys = calcRangeKeys({ - treeData, - expandedKeys: ['0-0', '0-2', '0-2-0'], - }); - expect(keys).toEqual([]); - }); - }); -}); diff --git a/components/tree/__tests__/util.test.tsx b/components/tree/__tests__/util.test.tsx new file mode 100644 index 0000000000..77642df7d8 --- /dev/null +++ b/components/tree/__tests__/util.test.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { calcRangeKeys } from '../utils/dictUtil'; +import renderSwitcherIcon from '../utils/iconUtil'; + +describe('Tree util', () => { + describe('calcRangeKeys', () => { + const treeData = [ + { key: '0-0', children: [{ key: '0-0-0' }, { key: '0-0-1' }] }, + { key: '0-1', children: [{ key: '0-1-0' }, { key: '0-1-1' }] }, + { + key: '0-2', + children: [ + { key: '0-2-0', children: [{ key: '0-2-0-0' }, { key: '0-2-0-1' }, { key: '0-2-0-2' }] }, + ], + }, + ]; + + it('calc range keys', () => { + const rangeKeys = calcRangeKeys({ + treeData, + expandedKeys: ['0-0', '0-2', '0-2-0'], + startKey: '0-2-0-1', + endKey: '0-0-0', + }); + const target = ['0-0-0', '0-0-1', '0-1', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1']; + expect(rangeKeys.sort()).toEqual(target.sort()); + }); + + it('return startKey when startKey === endKey', () => { + const keys = calcRangeKeys({ + treeData, + expandedKeys: ['0-0', '0-2', '0-2-0'], + startKey: '0-0-0', + endKey: '0-0-0', + }); + expect(keys).toEqual(['0-0-0']); + }); + + it('return empty array without startKey and endKey', () => { + const keys = calcRangeKeys({ + treeData, + expandedKeys: ['0-0', '0-2', '0-2-0'], + }); + expect(keys).toEqual([]); + }); + }); + + describe('renderSwitcherIcon', () => { + const prefixCls = 'tree'; + + it('returns a loading icon when loading', () => { + const { container } = render( + <>{renderSwitcherIcon(prefixCls, undefined, true, { loading: true })}, + ); + expect(container.getElementsByClassName(`${prefixCls}-switcher-loading-icon`)).toHaveLength( + 1, + ); + }); + + it('returns nothing when node is a leaf without showLine', () => { + const { container } = render( + <>{renderSwitcherIcon(prefixCls, undefined, false, { loading: false, isLeaf: true })}, + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('returns a custom leaf icon when provided', () => { + const testId = 'custom-icon'; + const customLeafIcon =
; + const { container } = render( + <> + {renderSwitcherIcon( + prefixCls, + undefined, + { showLeafIcon: customLeafIcon }, + { loading: false, isLeaf: true }, + )} + , + ); + + expect(screen.getByTestId(testId)).toBeVisible(); + expect( + container.getElementsByClassName(`${prefixCls}-switcher-line-custom-icon`), + ).toHaveLength(1); + }); + + it.each([ + [`${prefixCls}-switcher-line-icon`, true], + [`${prefixCls}-switcher-leaf-line`, false], + ])('returns %p element when showLeafIcon is %p', (expectedClassName, showLeafIcon) => { + const { container } = render( + <> + {renderSwitcherIcon( + prefixCls, + undefined, + { showLeafIcon }, + { loading: false, isLeaf: true }, + )} + , + ); + + expect(container.getElementsByClassName(expectedClassName)).toHaveLength(1); + }); + }); +}); diff --git a/components/tree/demo/line.md b/components/tree/demo/line.md index bb874c43eb..f0f372e349 100644 --- a/components/tree/demo/line.md +++ b/components/tree/demo/line.md @@ -14,8 +14,8 @@ title: Tree with connected line between nodes, turn on by `showLine`, customize the preseted icon by `switcherIcon`. ```tsx -import { CarryOutOutlined, FormOutlined } from '@ant-design/icons'; -import { Switch, Tree } from 'antd'; +import { CarryOutOutlined, CheckOutlined, FormOutlined } from '@ant-design/icons'; +import { Select, Switch, Tree } from 'antd'; import type { DataNode } from 'antd/es/tree'; import React, { useState } from 'react'; @@ -85,36 +85,44 @@ const treeData: DataNode[] = [ ]; const App: React.FC = () => { - const [showLine, setShowLine] = useState(true); + const [showLine, setShowLine] = useState(true); const [showIcon, setShowIcon] = useState(false); - const [showLeafIcon, setShowLeafIcon] = useState(true); + const [showLeafIcon, setShowLeafIcon] = useState(true); const onSelect = (selectedKeys: React.Key[], info: any) => { console.log('selected', selectedKeys, info); }; - const onSetLeafIcon = (checked: boolean) => { - setShowLeafIcon(checked); - setShowLine({ showLeafIcon: checked }); - }; + const handleLeafIconChange = (value: 'true' | 'false' | 'custom') => { + if (value === 'custom') { + return setShowLeafIcon(); + } - const onSetShowLine = (checked: boolean) => { - setShowLine(checked ? { showLeafIcon } : false); + if (value === 'true') { + return setShowLeafIcon(true); + } + + return setShowLeafIcon(false); }; return (
- showLine: + showLine:

showIcon:

- showLeafIcon: + showLeafIcon:{' '} +
ReactNode)} | false | | | switcherIcon | Customize collapse/expand icon of tree node | ReactNode \| ((props: AntTreeNodeProps) => ReactNode) | - | renderProps: 4.20.0 | | titleRender | Customize tree node title render | (nodeData) => ReactNode | - | 4.5.0 | | treeData | The treeNodes data Array, if set it then you need not to construct children TreeNode. (key should be unique across the whole array) | array<{ key, title, children, \[disabled, selectable] }> | - | | diff --git a/components/tree/index.zh-CN.md b/components/tree/index.zh-CN.md index c813be306b..e25c54a1c1 100644 --- a/components/tree/index.zh-CN.md +++ b/components/tree/index.zh-CN.md @@ -17,7 +17,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg ### Tree props | 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | +| --- | --- | --- | --- | --- | --- | | allowDrop | 是否允许拖拽时放置在该节点 | ({ dropNode, dropPosition }) => boolean | - | | | autoExpandParent | 是否自动展开父节点 | boolean | false | | | blockNode | 是否节点占据一行 | boolean | false | | @@ -44,7 +44,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg | selectable | 是否可选中 | boolean | true | | | selectedKeys | (受控)设置选中的树节点 | string\[] | - | | | showIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true,需要自行定义图标相关样式 | boolean | false | | -| showLine | 是否展示连接线 | boolean \| {showLeafIcon: boolean} | false | | +| showLine | 是否展示连接线 | boolean \| {showLeafIcon: boolean \| ReactNode | ((props: AntTreeNodeProps) => ReactNode)} | false | | | switcherIcon | 自定义树节点的展开/折叠图标 | ReactNode \| ((props: AntTreeNodeProps) => ReactNode) | - | renderProps: 4.20.0 | | titleRender | 自定义渲染节点 | (nodeData) => ReactNode | - | 4.5.0 | | treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点(key 在整个树范围内唯一) | array<{key, title, children, \[disabled, selectable]}> | - | | diff --git a/components/tree/utils/iconUtil.tsx b/components/tree/utils/iconUtil.tsx index 6c0bbbabc4..e275d335f4 100644 --- a/components/tree/utils/iconUtil.tsx +++ b/components/tree/utils/iconUtil.tsx @@ -6,12 +6,12 @@ import PlusSquareOutlined from '@ant-design/icons/PlusSquareOutlined'; import classNames from 'classnames'; import * as React from 'react'; import { cloneElement, isValidElement } from '../../_util/reactNode'; -import type { AntTreeNodeProps, SwitcherIcon } from '../Tree'; +import type { AntTreeNodeProps, TreeLeafIcon, SwitcherIcon } from '../Tree'; export default function renderSwitcherIcon( prefixCls: string, switcherIcon: SwitcherIcon, - showLine: boolean | { showLeafIcon: boolean } | undefined, + showLine: boolean | { showLeafIcon: boolean | TreeLeafIcon } | undefined, treeNodeProps: AntTreeNodeProps, ): React.ReactNode { const { isLeaf, expanded, loading } = treeNodeProps; @@ -19,18 +19,35 @@ export default function renderSwitcherIcon( if (loading) { return ; } - let showLeafIcon; + let showLeafIcon: boolean | TreeLeafIcon; if (showLine && typeof showLine === 'object') { showLeafIcon = showLine.showLeafIcon; } + if (isLeaf) { - if (showLine) { - if (typeof showLine === 'object' && !showLeafIcon) { - return ; - } - return ; + if (!showLine) { + return null; } - return null; + + if (typeof showLeafIcon !== 'boolean' && !!showLeafIcon) { + const leafIcon = + typeof showLeafIcon === 'function' ? showLeafIcon(treeNodeProps) : showLeafIcon; + const leafCls = `${prefixCls}-switcher-line-custom-icon`; + + if (isValidElement(leafIcon)) { + return cloneElement(leafIcon, { + className: classNames(leafIcon.props.className || '', leafCls), + }); + } + + return leafIcon; + } + + return showLeafIcon ? ( + + ) : ( + + ); } const switcherCls = `${prefixCls}-switcher-icon`;