Browse Source

feat: [Cascader] support CheckRelation API (#2594)

* feat: [Cascader] support CheckRelation API
YyumeiZhang 11 months ago
parent
commit
a218ee1f8a

+ 58 - 0
content/input/cascader/index-en-US.md

@@ -1537,6 +1537,63 @@ import { Cascader } from '@douyinfe/semi-ui';
 };
 ```
 
+### Checked RelationShip
+
+Version: >= 2.71.0
+
+In multiple, `checkRelation` can be used to set the type of node selection relationship, optional: 'related' (default), 'unRelated'. When the selection relationship is 'unRelated', it means that selections between nodes do not affect each other.
+
+```jsx live=true
+import React from 'react';
+import { Cascader } from '@douyinfe/semi-ui';
+() => {
+    const treeData = [
+        {
+            label: 'Asia',
+            value: 'Asia',
+            key: '0',
+            children: [
+                {
+                    label: 'China',
+                    value: 'China',
+                    key: '0-0',
+                    children: [
+                        {
+                            label: 'Beijing',
+                            value: 'Beijing',
+                            key: '0-0-0',
+                        },
+                        {
+                            label: 'Shanghai',
+                            value: 'Shanghai',
+                            key: '0-0-1',
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            label: 'North America',
+            value: 'North America',
+            key: '1',
+        }
+    ];
+    
+    return (
+        <Cascader
+            multiple
+            defaultValue={[
+                ['Asia'],
+                ['Asia', 'China', 'Beijing']
+            ]}
+            checkRelation='unRelated'
+            style={{ width: 300 }}
+            treeData={treeData}
+        />
+    );
+};
+```
+
 ### Dynamic Update of Data
 
 ```jsx live=true
@@ -1895,6 +1952,7 @@ function Demo() {
 | borderless        | borderless mode  >=2.33.0                                                                                                                                                                                                                     | boolean                         |           |
 | bottomSlot | bottom slot                                                                                                                                                                                                                                   | ReactNode | - |  1.27.0 |
 | changeOnSelect | Toggle whether non-leaf nodes are selectable                                                                                                                                                                                                  | boolean | false | - |
+| checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated'.  | string | 'related' | v2.71.0 |
 | className | ClassName                                                                                                                                                                                                                                     | string | - | - |
 | clearIcon | Can be used to customize the clear button, valid when showClear is true                                                                                                                                                                       | ReactNode | - | 2.25.0 |
 | defaultOpen | Set whether to open the dropDown by default                                                                                                                                                                                                   | boolean | false | - |

+ 51 - 0
content/input/cascader/index.md

@@ -1516,6 +1516,56 @@ import { Cascader } from '@douyinfe/semi-ui';
 };
 ```
 
+### 节点选中关系
+
+版本:>= 2.71.0
+
+多选时,可以使用 `checkRelation` 来设置节点之间选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated' 时,意味着节点之间的选中互不影响。
+
+```jsx live=true
+import React from 'react';
+import { Cascader } from '@douyinfe/semi-ui';
+() => {
+    const treeData = [
+        {
+            label: '亚洲',
+            value: 'Asia',
+            children: [
+                {
+                    label: '中国',
+                    value: 'China',
+                    children: [
+                        {
+                            label: '北京',
+                            value: 'Beijing',
+                        },
+                        {
+                            label: '上海',
+                            value: 'Shanghai',
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            label: '北美洲',
+            value: 'North America',
+        }
+    ];
+    return (
+        <Cascader
+            multiple
+            defaultValue={[
+                ['Asia'],
+                ['Asia', 'China', 'Beijing']
+            ]}
+            checkRelation='unRelated'
+            style={{ width: 300 }}
+            treeData={treeData}
+        />
+    );
+};
+```
 
 ### 动态更新数据
 
@@ -1872,6 +1922,7 @@ function Demo() {
 | bottomSlot           | 底部插槽                                                                                                                                                | ReactNode                                                                                 | -                              | 1.27.0 |
 | borderless        | 无边框模式  >=2.33.0                                                                                                                                     | boolean                         |           |
 | changeOnSelect       | 是否允许选择非叶子节点                                                                                                                                         | boolean                                                                                   | false                          | -      |
+| checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated'。  | string | 'related' | v2.71.0 |
 | className            | 选择框的 className 属性                                                                                                                                   | string                                                                                    | -                              | -      |
 | clearIcon            | 可用于自定义清除按钮, showClear为true时有效                                                                                                                       | ReactNode                                                                                 | -                              | 2.25.0 |
 | defaultOpen          | 设置是否默认打开下拉菜单                                                                                                                                        | boolean                                                                                   | false                          | -      |

+ 11 - 0
cypress/e2e/cascader.spec.js

@@ -128,5 +128,16 @@ describe('cascader', () => {
         cy.get('.semi-input').type('{esc}', { force: true });
         cy.get('.semi-cascader-popover').should('not.exist');
     })
+
+    it('unRelated', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=cascader--un-related');
+        cy.get('.semi-cascader-selection-tag').should('have.length', 2);
+        cy.get('.semi-cascader-selection-tag').eq(0).contains('亚洲');
+        cy.get('.semi-cascader-selection-tag').eq(1).contains('美国');
+        cy.get('.semi-cascader-selection').eq(0).trigger('click');
+        cy.get('.semi-checkbox').eq(1).click();
+        cy.get('.semi-cascader-selection-tag').should('have.length', 3);
+        cy.get('.semi-cascader-selection-tag').eq(2).contains('北美洲');
+    })
     
 });

+ 2 - 0
packages/semi-foundation/cascader/constants.ts

@@ -18,6 +18,8 @@ const strings = {
     NONE_MERGE_TYPE: 'none',
     SEARCH_POSITION_TRIGGER: 'trigger',
     SEARCH_POSITION_CUSTOM: 'custom',
+    RELATED: 'related',
+    UN_RELATED: 'unRelated'
 } as const;
 
 const numbers = {};

+ 49 - 2
packages/semi-foundation/cascader/foundation.ts

@@ -168,6 +168,7 @@ export interface BasicCascaderProps {
     enableLeafClick?: boolean;
     preventScroll?: boolean;
     virtualizeInSearch?: Virtualize;
+    checkRelation?: string;
     onClear?: () => void;
     triggerRender?: (props: BasicTriggerRenderProps) => any;
     onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
@@ -591,7 +592,7 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
         }
     }
 
-    updateSearching = (isSearching: boolean)=>{
+    updateSearching = (isSearching: boolean) => {
         this._adapter.updateStates({ isSearching: false });
     }
 
@@ -772,6 +773,16 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
     }
 
     _handleMultipleSelect(item: BasicEntity | BasicData) {
+        const { checkRelation } = this.getProps();
+        if (checkRelation === strings.RELATED) {
+            this._handleRelatedMultipleSelect(item);
+        } else if (checkRelation === 'unRelated') {
+            this._handleUnRelatedMultipleSelect(item);
+        }
+        this._adapter.updateStates({ inputValue: '' });
+    }
+
+    _handleRelatedMultipleSelect(item: BasicEntity | BasicData) {
         const { key } = item;
         const { checkedKeys, keyEntities, resolvedCheckedKeys } = this.getStates();
         const { autoMergeValue, max, disableStrictly, leafOnly } = this.getProps();
@@ -837,8 +848,44 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
         if (curCheckedStatus) {
             this._notifySelect(curRealCheckedKeys);
         }
+    }
 
-        this._adapter.updateStates({ inputValue: '' });
+    _handleUnRelatedMultipleSelect(item: BasicEntity | BasicData) {
+        const { key } = item;
+        const { checkedKeys, keyEntities } = this.getStates();
+        const { max } = this.getProps();
+        const newCheckedKeys: Set<string> = new Set(checkedKeys);
+        let targetStatus: boolean;
+        const prevCheckedStatus = checkedKeys.has(key);
+        if (prevCheckedStatus) {
+            newCheckedKeys.delete(key);
+            targetStatus = false;
+        } else {
+            // 查看是否超出 max
+            if (isNumber(max)) {
+                if (checkedKeys.size >= max) {
+                    const checkedEntities: BasicEntity[] = [];
+                    checkedKeys.forEach(itemKey => {
+                        checkedEntities.push(keyEntities[itemKey]);
+                    });
+                    this._adapter.notifyOnExceed(checkedEntities);
+                    return;
+                }
+            }
+            newCheckedKeys.add(key);
+            targetStatus = true;
+        }
+        if (!this._isControlledComponent()) {
+            this._adapter.updateStates({
+                checkedKeys: newCheckedKeys,
+            });
+        }
+
+        this._notifyChange(newCheckedKeys);
+
+        if (targetStatus) {
+            this._notifySelect(newCheckedKeys);
+        }
     }
 
     calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {

+ 28 - 0
packages/semi-ui/cascader/_story/cascader.stories.jsx

@@ -2442,4 +2442,32 @@ export const CustomExpandIcon = () => {
       />
     </>
   );
+}
+
+export const UnRelated = () => {
+  const [value, setValue] = useState([
+    [ "yazhou" ],
+    [ "beimeizhou", "meiguo"],
+  ]);
+
+  const onChange = useCallback((value) => {
+    setValue(value);
+  }, [])
+
+  const onSelect = useCallback((value) => {
+    console.log('onSelect', value);
+  }, [])
+
+  return (
+    <Cascader
+      style={{ width: 400 }}
+      treeData={treeData2}
+      value={value}
+      filterTreeNode
+      multiple
+      checkRelation='unRelated'
+      onChange={onChange}
+      onSelect={onSelect}
+    />
+  )
 }

+ 22 - 15
packages/semi-ui/cascader/index.tsx

@@ -219,6 +219,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         enableLeafClick: false,
         'aria-label': 'Cascader',
         searchPosition: strings.SEARCH_POSITION_TRIGGER,
+        checkRelation: strings.RELATED,
     })
 
     options: any;
@@ -429,7 +430,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     }
 
     static getDerivedStateFromProps(props: CascaderProps, prevState: CascaderState) {
-        const { multiple, value, defaultValue, onChangeWithObject, leafOnly, autoMergeValue } = props;
+        const { multiple, value, defaultValue, onChangeWithObject, leafOnly, autoMergeValue, checkRelation } = props;
         const { prevProps } = prevState;
         let keyEntities = prevState.keyEntities || {};
         const newState: Partial<CascaderState> = {};
@@ -498,18 +499,22 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
                 if (isSet(realKeys)) {
                     realKeys = [...realKeys];
                 }
-                const calRes = calcCheckedKeys(realKeys, keyEntities);
-                const checkedKeys = new Set(calRes.checkedKeys);
-                const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
-                // disableStrictly
-                if (props.disableStrictly) {
-                    newState.disabledKeys = calcDisabledKeys(keyEntities);
+                if (checkRelation === strings.RELATED) {
+                    const calRes = calcCheckedKeys(realKeys, keyEntities);
+                    const checkedKeys = new Set(calRes.checkedKeys);
+                    const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
+                    // disableStrictly
+                    if (props.disableStrictly) {
+                        newState.disabledKeys = calcDisabledKeys(keyEntities);
+                    }
+                    const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
+                    newState.checkedKeys = checkedKeys;
+                    newState.halfCheckedKeys = halfCheckedKeys;
+                    newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
+                } else {
+                    newState.checkedKeys = new Set(realKeys);
                 }
-                const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
                 newState.prevProps = props;
-                newState.checkedKeys = checkedKeys;
-                newState.halfCheckedKeys = halfCheckedKeys;
-                newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
             }
         }
         return newState;
@@ -594,10 +599,11 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     };
 
     renderTagInput() {
-        const { size, disabled, placeholder, maxTagCount, showRestTagsPopover, restTagsPopoverProps } = this.props;
+        const { size, disabled, placeholder, maxTagCount, showRestTagsPopover, restTagsPopoverProps, checkRelation } = this.props;
         const { inputValue, checkedKeys, keyEntities, resolvedCheckedKeys } = this.state;
         const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
-        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE ? checkedKeys : resolvedCheckedKeys;
+        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE  || checkRelation === strings.UN_RELATED ?
+            checkedKeys : resolvedCheckedKeys;
         return (
             <TagInput
                 className={tagInputcls}
@@ -765,9 +771,10 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     };
 
     renderMultipleTags = () => {
-        const { autoMergeValue, maxTagCount } = this.props;
+        const { autoMergeValue, maxTagCount, checkRelation } = this.props;
         const { checkedKeys, resolvedCheckedKeys } = this.state;
-        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE ? checkedKeys : resolvedCheckedKeys;
+        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE || checkRelation === strings.UN_RELATED ? 
+            checkedKeys : resolvedCheckedKeys;
         const displayTag: Array<ReactNode> = [];
         const hiddenTag: Array<ReactNode> = [];
         [...realKeys].forEach((checkedKey, idx) => {