From 29de7e5b38641c021acff2f46fce5e2d14dd4cfd Mon Sep 17 00:00:00 2001 From: Lu Yang <58989501+LuyangFE@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:33:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E8=8A=82=E7=82=B9(=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E7=88=B6=E8=8A=82=E7=82=B9)=20(#2233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 显示所有选中节点(包括父节点) * feat: 显示所有选中节点(包括父节点) * feat: 更新文档 * feat: Tree/TreeSelect add autoMergeValue API * chore: update yarn.lock file * chore: delete console.log * chore: Remove unnecessary parameters --------- Co-authored-by: zhangyumei.0319 --- content/input/treeselect/index-en-US.md | 1 + content/input/treeselect/index.md | 1 + content/navigation/tree/index-en-US.md | 1 + content/navigation/tree/index.md | 1 + packages/semi-foundation/tree/foundation.ts | 4 +- .../semi-foundation/treeSelect/foundation.ts | 4 +- .../tree/__test__/treeMultiple.test.js | 21 ++++++ packages/semi-ui/tree/_story/tree.stories.jsx | 22 +++++++ packages/semi-ui/tree/index.tsx | 2 + packages/semi-ui/tree/interface.ts | 3 +- .../treeSelect/__test__/treeMultiple.test.js | 24 +++++++ .../treeSelect/_story/treeSelect.stories.jsx | 22 +++++++ packages/semi-ui/treeSelect/index.tsx | 64 ++++++++----------- 13 files changed, 127 insertions(+), 43 deletions(-) diff --git a/content/input/treeselect/index-en-US.md b/content/input/treeselect/index-en-US.md index 4c8eb09f1a..18b3b63608 100644 --- a/content/input/treeselect/index-en-US.md +++ b/content/input/treeselect/index-en-US.md @@ -1412,6 +1412,7 @@ function Demo() { | arrowIcon|Customize the right drop-down arrow Icon, when the showClear switch is turned on and there is currently a selected value, hover will give priority to the clear icon| ReactNode | | 1.15.0| |autoAdjustOverflow|Whether the pop-up layer automatically adjusts the direction when it is obscured (only vertical direction is supported for the time being, and the inserted parent is body)|boolean | true| 0.34.0| | autoExpandParent | Toggle whether to expand parent nodes automatically | boolean | false | 0.34.0 | +| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, value will include that node and its children. (Works if leafOnly is false)| boolean | false | 2.60.0 | | borderless | borderless mode >=2.33.0 | boolean | | | checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated' | string | 'related' | 2.5.0 | | className | Class name | string | - | - | diff --git a/content/input/treeselect/index.md b/content/input/treeselect/index.md index e86ea55d71..dd397043f9 100644 --- a/content/input/treeselect/index.md +++ b/content/input/treeselect/index.md @@ -1395,6 +1395,7 @@ function Demo() { | arrowIcon| 自定义右侧下拉箭头Icon,当showClear开关打开且当前有选中值时,hover会优先显示clear icon | ReactNode | | 1.15.0| | autoAdjustOverflow| 浮层被遮挡时是否自动调整方向(暂时仅支持竖直方向,且插入的父级为 body) |boolean | true| 0.34.0| | autoExpandParent | 是否自动展开父节点 | boolean | false | 0.34.0 | +| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将包括该节点以及该子孙节点。(在leafOnly为false的情况下生效)| boolean | false | 2.60.0 | | borderless | 无边框模式 >=2.33.0 | boolean | | | checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated' | string | 'related' | 2.5.0 | | className | 选择框的 `className` 属性 | string | - | - | diff --git a/content/navigation/tree/index-en-US.md b/content/navigation/tree/index-en-US.md index b0b2bf73cf..c7095ab6bc 100644 --- a/content/navigation/tree/index-en-US.md +++ b/content/navigation/tree/index-en-US.md @@ -2271,6 +2271,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec | ------------------- | --------------------- | ------------------------------------------------- | ------- | ------ | | autoExpandParent | Toggle whether to expand parent node automatically | boolean | false | 0.34.0 | | autoExpandWhenDragEnter | Toggle whether allow autoExpand when drag enter node | boolean | true | 1.8.0 | +| autoMergeValue | Sets the automerge value. Specifically, when enabled, when a parent node is selected, value will include that node and its children. (Works if leafOnly is false)| boolean | false | 2.60.0 | | blockNode | Toggle whether to display node as row | boolean | true | - | | checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated' | string | 'related' | 2.5.0 | | className | Class name| string | - | - | diff --git a/content/navigation/tree/index.md b/content/navigation/tree/index.md index d9494c002a..52906b7333 100644 --- a/content/navigation/tree/index.md +++ b/content/navigation/tree/index.md @@ -2286,6 +2286,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec |------------- | ----------- | -------------- | -------------- | --------| | autoExpandParent | 是否自动展开父节点,默认为 false,当组件初次挂载时为 true | boolean | false | 0.34.0 | | autoExpandWhenDragEnter | 是否允许拖拽到节点上时自动展开改节点 | boolean | true | 1.8.0 | +| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将包括该节点以及该子孙节点。(在leafOnly为false的情况下生效)| boolean | false | 2.60.0 | | blockNode | 行显示节点 | boolean | true | - | | checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated' | string | 'related' | 2.5.0 | | className | 类名 | string | - | - | diff --git a/packages/semi-foundation/tree/foundation.ts b/packages/semi-foundation/tree/foundation.ts index 8eb68f4bcf..eb11d45051 100644 --- a/packages/semi-foundation/tree/foundation.ts +++ b/packages/semi-foundation/tree/foundation.ts @@ -456,11 +456,11 @@ export default class TreeFoundation extends BaseFoundation, S = Record { expect(spyOnChange.calledWithMatch(['fish', 'Yazhou'])).toEqual(true); tree.unmount(); }) + + it('onChange + autoMergeValue', () => { + let spyOnSelect = sinon.spy(() => { }); + let spyOnChange = sinon.spy(() => { }); + let treeSelect = getTree({ + defaultExpandAll: true, + onSelect: spyOnSelect, + onChange: spyOnChange, + autoMergeValue: false, + }); + let nodeChina = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`).at(0); + // select China + nodeChina.simulate('click'); + // onSelect & onChange + expect(spyOnSelect.calledOnce).toBe(true); + expect(spyOnChange.calledOnce).toBe(true); + expect(spyOnSelect.calledWithMatch('zhongguo', true, { key: "zhongguo" })).toEqual(true); + expect(spyOnChange.calledWithMatch( + ['Zhongguo', 'Beijing', 'Shanghai'], + )).toEqual(true); + }); }) diff --git a/packages/semi-ui/tree/_story/tree.stories.jsx b/packages/semi-ui/tree/_story/tree.stories.jsx index 375ba14c27..6ec495ff5a 100644 --- a/packages/semi-ui/tree/_story/tree.stories.jsx +++ b/packages/semi-ui/tree/_story/tree.stories.jsx @@ -3116,4 +3116,26 @@ export const CustomRenderIcon = () => { treeData={treeDataWithSuffix} icon={iconFunc} />); +} + +export const AutoMerge = () => { + const [value, setValue] = useState([]); + + const onChange = useCallback((val) => { + console.log('onChange', val); + setValue(val); + }, []); + + return ( + <> + + + ) } \ No newline at end of file diff --git a/packages/semi-ui/tree/index.tsx b/packages/semi-ui/tree/index.tsx index 50c5cb9c4d..c42cc9f5f0 100644 --- a/packages/semi-ui/tree/index.tsx +++ b/packages/semi-ui/tree/index.tsx @@ -52,6 +52,7 @@ class Tree extends BaseComponent { static contextType = ConfigContext; static propTypes = { + autoMergeValue: PropTypes.bool, blockNode: PropTypes.bool, className: PropTypes.string, showClear: PropTypes.bool, @@ -140,6 +141,7 @@ class Tree extends BaseComponent { draggable: false, autoExpandWhenDragEnter: true, checkRelation: 'related', + autoMergeValue: true, }; static TreeNode: typeof TreeNode; diff --git a/packages/semi-ui/tree/interface.ts b/packages/semi-ui/tree/interface.ts index 7606695a75..84dd042700 100644 --- a/packages/semi-ui/tree/interface.ts +++ b/packages/semi-ui/tree/interface.ts @@ -84,7 +84,8 @@ export interface TreeProps extends BasicTreeProps { onSelect?: (selectedKey: string, selected: boolean, selectedNode: TreeNodeData) => void; renderDraggingNode?: (nodeInstance: HTMLElement, node: TreeNodeData) => HTMLElement; renderFullLabel?: (renderFullLabelProps: RenderFullLabelProps) => ReactNode; - renderLabel?: (label?: ReactNode, treeNode?: TreeNodeData) => ReactNode + renderLabel?: (label?: ReactNode, treeNode?: TreeNodeData) => ReactNode; + autoMergeValue?: boolean } export interface OptionProps { index: number; diff --git a/packages/semi-ui/treeSelect/__test__/treeMultiple.test.js b/packages/semi-ui/treeSelect/__test__/treeMultiple.test.js index 6486d6ea39..654f9433c0 100644 --- a/packages/semi-ui/treeSelect/__test__/treeMultiple.test.js +++ b/packages/semi-ui/treeSelect/__test__/treeMultiple.test.js @@ -1129,4 +1129,28 @@ describe('TreeSelect', () => { let selectContentNode = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-select-selection`).at(0); expect(selectContentNode.find(`.${BASE_CLASS_PREFIX}-tag-content`).instance().textContent).toEqual('中国'); }); + + it('onChange + autoMergeValue', () => { + let spyOnSelect = sinon.spy(() => { }); + let spyOnChange = sinon.spy(() => { }); + let treeSelect = getTreeSelect({ + defaultExpandAll: true, + onSelect: spyOnSelect, + onChange: spyOnChange, + autoMergeValue: false, + }); + let nodeChina = treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`).at(0); + // select China + nodeChina.simulate('click'); + // onSelect & onChange + expect(spyOnSelect.calledOnce).toBe(true); + expect(spyOnChange.calledOnce).toBe(true); + expect(spyOnSelect.calledWithMatch('zhongguo', true, { key: "zhongguo" })).toEqual(true); + expect(spyOnChange.calledWithMatch( + ['Zhongguo', 'Beijing', 'Shanghai'], + )).toEqual(true); + + let tagGroup = treeSelect.find(`.${BASE_CLASS_PREFIX}-tag`); + expect(tagGroup.length).toEqual(3); + }); }) diff --git a/packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx b/packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx index 30ff77fe97..2ffeab5c03 100644 --- a/packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx +++ b/packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx @@ -2791,3 +2791,25 @@ export const CustomSelectAll = () => { ); }; + +export const AutoMerge = () => { + const [value, setValue] = useState([]); + + const onChange = useCallback((val) => { + console.log('onChange', val); + setValue(val); + }, []); + + return ( + <> + + + ) +} diff --git a/packages/semi-ui/treeSelect/index.tsx b/packages/semi-ui/treeSelect/index.tsx index 1e3555b1cd..621e31e7a7 100644 --- a/packages/semi-ui/treeSelect/index.tsx +++ b/packages/semi-ui/treeSelect/index.tsx @@ -150,7 +150,8 @@ export interface TreeSelectProps extends Omit void; onVisibleChange?: (isVisible: boolean) => void; - onClear?: (e: React.MouseEvent | React.KeyboardEvent) => void + onClear?: (e: React.MouseEvent | React.KeyboardEvent) => void; + autoMergeValue?: boolean } export type OverrideCommonState = @@ -271,6 +272,7 @@ class TreeSelect extends BaseComponent { restTagsPopoverProps: PropTypes.object, preventScroll: PropTypes.bool, clickTriggerToHide: PropTypes.bool, + autoMergeValue: PropTypes.bool, }; static defaultProps: Partial = { @@ -304,6 +306,7 @@ class TreeSelect extends BaseComponent { showRestTagsPopover: false, restTagsPopoverProps: {}, clickTriggerToHide: true, + autoMergeValue: true, }; inputRef: React.RefObject; tagInputRef: React.RefObject; @@ -859,15 +862,14 @@ class TreeSelect extends BaseComponent { return showClear && (this.hasValue() || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering); }; - renderTagList = () => { - const { checkedKeys, keyEntities, disabledKeys, realCheckedKeys } = this.state; + renderTagList = (triggerRenderKeys: string[]) => { + const { keyEntities, disabledKeys } = this.state; const { treeNodeLabelProp, leafOnly, disabled, disableStrictly, size, - checkRelation, renderSelectedItem: propRenderSelectedItem, keyMaps } = this.props; @@ -878,14 +880,8 @@ class TreeSelect extends BaseComponent { isRenderInTag: true, content: get(item, realLabelName, null) }); - let renderKeys = []; - if (checkRelation === 'related') { - renderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true); - } else if (checkRelation === 'unRelated' && Object.keys(keyEntities).length > 0) { - renderKeys = [...realCheckedKeys]; - } const tagList: Array = []; - renderKeys.forEach((key: TreeNodeData['key'], index) => { + triggerRenderKeys.forEach((key: TreeNodeData['key'], index) => { const item = (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key); const onClose = (tagContent: any, e: React.MouseEvent) => { if (e && typeof e.preventDefault === 'function') { @@ -950,7 +946,7 @@ class TreeSelect extends BaseComponent { ); }; - renderSelectContent = () => { + renderSelectContent = (triggerRenderKeys: string[]) => { const { multiple, placeholder, @@ -963,7 +959,7 @@ class TreeSelect extends BaseComponent { const isTriggerPositionSearch = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER; // searchPosition = trigger if (isTriggerPositionSearch) { - return multiple ? this.renderTagInput() : this.renderSingleTriggerSearch(); + return multiple ? this.renderTagInput(triggerRenderKeys) : this.renderSingleTriggerSearch(); } // searchPosition = dropdown and single seleciton if (!multiple || !this.hasValue()) { @@ -974,7 +970,7 @@ class TreeSelect extends BaseComponent { return {renderText ? renderText : placeholder}; } // searchPosition = dropdown and multiple seleciton - const tagList = this.renderTagList(); + const tagList = this.renderTagList(triggerRenderKeys); // mode=custom to return tagList directly return ( @@ -1071,6 +1067,7 @@ class TreeSelect extends BaseComponent { searchPosition, triggerRender, borderless, + autoMergeValue, checkRelation, ...rest } = this.props; @@ -1110,17 +1107,19 @@ class TreeSelect extends BaseComponent { className ); let inner: React.ReactNode | React.ReactNode[]; - if (useCustomTrigger) { - let triggerRenderKeys = []; - if (multiple) { - if (checkRelation === 'related') { - triggerRenderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true); - } else if (checkRelation === 'unRelated') { - triggerRenderKeys = [...realCheckedKeys]; - } - } else { - triggerRenderKeys = selectedKeys; + let triggerRenderKeys = []; + if (multiple) { + if (!autoMergeValue) { + triggerRenderKeys =[...checkedKeys]; + } else if (checkRelation === 'related') { + triggerRenderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true); + } else if (checkRelation === 'unRelated') { + triggerRenderKeys = [...realCheckedKeys]; } + } else { + triggerRenderKeys = selectedKeys; + } + if (useCustomTrigger) { inner = get(keyEntities, [key, 'data']))} @@ -1137,7 +1136,7 @@ class TreeSelect extends BaseComponent { inner = [ {prefix || insetLabel ? this.renderPrefix() : null}, -
{this.renderSelectContent()}
+
{this.renderSelectContent(triggerRenderKeys)}
, {suffix ? this.renderSuffix() : null}, @@ -1238,15 +1237,13 @@ class TreeSelect extends BaseComponent { ); }; - renderTagInput = () => { + renderTagInput = (triggerRenderKeys: string[]) => { const { - leafOnly, disabled, size, searchAutoFocus, placeholder, maxTagCount, - checkRelation, showRestTagsPopover, restTagsPopoverProps, searchPosition, @@ -1254,17 +1251,8 @@ class TreeSelect extends BaseComponent { preventScroll } = this.props; const { - keyEntities, - checkedKeys, inputValue, - realCheckedKeys, } = this.state; - let keyList = []; - if (checkRelation === 'related') { - keyList = normalizeKeyList(checkedKeys, keyEntities, leafOnly, true); - } else if (checkRelation === 'unRelated') { - keyList = [...realCheckedKeys]; - } // auto focus search input divide into two parts // 1. filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER // Implemented by passing autofocus to the underlying input's autofocus @@ -1280,7 +1268,7 @@ class TreeSelect extends BaseComponent { onInputChange={v => this.search(v)} ref={this.tagInputRef} placeholder={placeholder} - value={keyList} + value={triggerRenderKeys} inputValue={inputValue} size={size} showRestTagsPopover={showRestTagsPopover}