فهرست منبع

Merge branch 'main' into release

pointhalo 1 سال پیش
والد
کامیت
5fb609e4f5

+ 6 - 0
cypress/e2e/table.spec.js

@@ -240,4 +240,10 @@ describe('table', () => {
         cy.get('[data-cy=section] .semi-table-expand-icon').eq(0).click({ force: true });
         cy.get('[data-cy=section] .semi-table-row-hidden').should('have.length', 7);
     });
+
+    it('test header selected status when all rows are disabled and selected ', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=table--fixed-all-disabled-and-selected&viewMode=story');
+        cy.get('button').contains('点击全选').click();
+        cy.get('.semi-table-thead .semi-checkbox-checked').should('exist')
+    });
 });

+ 10 - 6
packages/semi-foundation/autoComplete/foundation.ts

@@ -235,7 +235,7 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
 
         const options = this._generateList(data);
         // Get the option whose value match from options
-        let selectedOption: StateOptionItem | Array<StateOptionItem> = options.filter(option => renderSelectedItem(option) === selectedValue);
+        let selectedOption: StateOptionItem | Array<StateOptionItem> = options.length ? options.filter(option => renderSelectedItem(option) === selectedValue) : [];
         const canMatchInData = selectedOption.length;
 
         const selectedOptionIndex = options.findIndex(option => renderSelectedItem(option) === selectedValue);
@@ -262,11 +262,13 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
 
         let { data, defaultActiveFirstOption } = this.getProps();
 
-        let renderSelectedItem = this._getRenderSelectedItem();
-
-        const options = this._generateList(data);
+        let selectedOptionIndex = -1;
 
-        const selectedOptionIndex = options.findIndex(option => renderSelectedItem(option) === searchValue);
+        if (searchValue) {
+            let renderSelectedItem = this._getRenderSelectedItem();
+            const options = this._generateList(data);
+            selectedOptionIndex = options.findIndex(option => renderSelectedItem(option) === searchValue);
+        }
 
         if (selectedOptionIndex === -1 && defaultActiveFirstOption) {
             if (focusIndex !== 0) {
@@ -288,7 +290,9 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
         let { renderSelectedItem } = this.getProps();
 
         if (typeof renderSelectedItem === 'undefined') {
-            renderSelectedItem = (option: any) => option.value;
+            renderSelectedItem = (option: any) => {
+                return option?.value;
+            };
         } else if (renderSelectedItem && typeof renderSelectedItem === 'function') {
             // do nothing
         }

+ 3 - 1
packages/semi-foundation/cascader/constants.ts

@@ -24,4 +24,6 @@ export {
     cssClasses,
     strings,
     numbers
-};
+};
+
+export const VALUE_SPLIT = '_SEMI_CASCADER_SPLIT_';

+ 11 - 13
packages/semi-foundation/cascader/foundation.ts

@@ -1,4 +1,4 @@
-import { isEqual, get, difference, isUndefined, assign, cloneDeep, isEmpty, isNumber, includes, isFunction } from 'lodash';
+import { isEqual, get, difference, isUndefined, assign, cloneDeep, isEmpty, isNumber, includes, isFunction, isObject } from 'lodash';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import {
     filter,
@@ -12,10 +12,10 @@ import {
 import { Motion } from '../utils/type';
 import {
     convertDataToEntities,
-    findKeysForValues,
     normalizedArr,
     isValid,
-    calcMergeType
+    calcMergeType,
+    getKeysByValuePath
 } from './util';
 import { strings } from './constants';
 import isEnterPress from '../utils/isEnterPress';
@@ -439,14 +439,14 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
         const loadingKeys = this._adapter.getLoadingKeyRefValue();
         const filterable = this._isFilterable();
         const loadingActive = [...activeKeys].filter(i => loadingKeys.has(i));
-
-        const valuePath = onChangeWithObject ? normalizedArr(value).map(i => i.value) : normalizedArr(value);
-        const selectedKeys = findKeysForValues(valuePath, keyEntities);
+        const normalizedValue = normalizedArr(value);
+        const valuePath = onChangeWithObject && isObject(normalizedValue[0]) ? normalizedValue.map(i => i.value) : normalizedValue;
+        const selectedKeys = getKeysByValuePath(valuePath);
         let updateStates: Partial<BasicCascaderInnerData> = {};
 
-        if (selectedKeys.length) {
-            const selectedKey = selectedKeys[0];
-            const selectedItem = keyEntities[selectedKey];
+        const selectedKey = selectedKeys.length > 0 ? selectedKeys[0] : undefined;
+        const selectedItem = selectedKey ? keyEntities[selectedKey] : undefined;
+        if (selectedItem) {
             /**
              * When changeOnSelect is turned on, or the target option is a leaf option,
              * the option is considered to be selected, even if the option is disabled
@@ -874,10 +874,8 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
         const { keyEntities } = this.getStates();
         const values: (string | number)[] = [];
         keys.forEach(key => {
-            if (!isEmpty(keyEntities) && !isEmpty(keyEntities[key])) {
-                const valueItem = keyEntities[key].data.value;
-                values.push(valueItem);
-            }
+            const valueItem = keyEntities[key]?.data?.value;
+            valueItem !== undefined && values.push(valueItem);
         });
         const formatValue: number | string | Array<string | number> = values.length === 1 ?
             values[0] :

+ 21 - 10
packages/semi-foundation/cascader/util.ts

@@ -3,7 +3,7 @@ import {
     isUndefined,
     isEqual
 } from 'lodash';
-import { strings } from './constants';
+import { strings, VALUE_SPLIT } from './constants';
 
 function getPosition(level: any, index: any) {
     return `${level}-${index}`;
@@ -30,7 +30,7 @@ function traverseDataNodes(treeNodes: any, callback: any) {
         let item: any = null;
         // Process node if is not root
         if (node) {
-            const key = parent ? getPosition(parent.key, ind) : `${ind}`;
+            const key = parent ? `${parent.key}${VALUE_SPLIT}${node.value}` : node.value;
             item = {
                 data: { ...node },
                 ind,
@@ -55,6 +55,25 @@ function traverseDataNodes(treeNodes: any, callback: any) {
     processNode(null);
 }
 
+export function getKeysByValuePath(valuePath: (string | number)[][] | (string | number)[]) {
+    if (valuePath?.length) {
+        if (Array.isArray(valuePath[0])) {
+            return valuePath.map((item) => getKeyByValuePath(item));
+        } else {
+            return [getKeyByValuePath(valuePath as (string | number)[])];
+        }
+    }
+    return [];
+}
+
+export function getKeyByValuePath(valuePath: (string | number)[]) {
+    return valuePath.join(VALUE_SPLIT);
+}
+
+export function getValuePathByKey(key: string) {
+    return key.split(VALUE_SPLIT);
+}
+
 export function convertDataToEntities(dataNodes: any) {
     const keyEntities: any = {};
 
@@ -74,14 +93,6 @@ export function convertDataToEntities(dataNodes: any) {
     return keyEntities;
 }
 
-export function findKeysForValues(value: any, keyEntities: any) {
-    const valuePath = normalizedArr(value);
-    const res = Object.values(keyEntities)
-        .filter((item: any) => isEqual(item.valuePath, valuePath))
-        .map((item: any) => item.key);
-    return res;
-}
-
 export function calcMergeType(autoMergeValue: boolean, leafOnly: boolean): string {
     let mergeType: string;
     if (leafOnly) {

+ 2 - 1
packages/semi-foundation/table/foundation.ts

@@ -989,7 +989,8 @@ class TableFoundation<RecordType> extends BaseFoundation<TableAdapter<RecordType
             }
             return true;
         } else {
-            return false;
+            const isAllSelected = allKeys.every(rowKey => selectedRowKeysSet.has(rowKey));
+            return isAllSelected || false;
         }
     }
 

+ 314 - 291
packages/semi-ui/autoComplete/_story/autoComplete.stories.jsx

@@ -2,343 +2,342 @@ import React, { Component, useState } from 'react';
 
 import CustomTrigger from './CustomTrigger';
 import AutoComplete from '../index';
+import { Form } from '../../index';
 import { IconSearch } from '@douyinfe/semi-icons';
 
 export default {
-  title: 'AutoComplete',
-  parameters: {
-    chromatic: { disableSnapshot: true },
-  },
-}
+    title: 'AutoComplete',
+    parameters: {
+        chromatic: { disableSnapshot: true },
+    },
+};
 
 const props = {
-  onBlur: (v, e) => {
-    console.log('onBlur');
-    console.log(v, e);
-  },
-  onFocus: (v, e) => {
-    console.log('onFocus');
-    console.log(v, e);
-  },
+    onBlur: (v, e) => {
+        console.log('onBlur');
+        console.log(v, e);
+    },
+    onFocus: (v, e) => {
+        console.log('onFocus');
+        console.log(v, e);
+    },
 };
 
 class Demo extends React.Component {
-  constructor() {
-    super();
-    this.state = {
-      data: [],
-      data2: ['mike', 'tony', 'steve'],
-    };
-    this.acref = React.createRef();
-  }
-
-  handleSearch(value) {
-    // let data =  !value ? [] : [value, value + value, value + value + value];
-    let result; // if (!value || value.indexOf('@') >= 0) {
-    //     result = [];
-    // } else {
-
-    if (value) {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
-    } else {
-      result = [];
-    } // }
-
-    this.setState({
-      data: result,
-    });
-  }
-
-  handleSearch2(value) {
-    // let data2 =  !value ? [] : [value, value + value, value + value + value];
-    let result;
-
-    if (!value || value.indexOf('@') >= 0) {
-      result = [];
-    } else {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+    constructor() {
+        super();
+        this.state = {
+            data: [],
+            data2: ['mike', 'tony', 'steve'],
+        };
+        this.acref = React.createRef();
     }
 
-    this.setState({
-      data2: result,
-    });
-  }
-
-  render() {
-    const { data, data2 } = this.state;
-    return (
-      <div>
-        <AutoComplete
-          placeholder="fe"
-          className="test-ac"
-          prefix={<IconSearch />}
-          showClear
-          data={data}
-          style={{
-            width: 300,
-          }}
-          onSearch={this.handleSearch.bind(this)}
-          onSelect={v => console.log(v)}
-          {...props}
-          ref={this.acref}
-        />
-      </div>
-    );
-  }
-}
+    handleSearch(value) {
+        // let data =  !value ? [] : [value, value + value, value + value + value];
+        let result; // if (!value || value.indexOf('@') >= 0) {
+        //     result = [];
+        // } else {
+
+        if (value) {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+        } else {
+            result = [];
+        } // }
+
+        this.setState({
+            data: result,
+        });
+    }
 
-export const BasicUsage = () => <Demo />;
+    handleSearch2(value) {
+        // let data2 =  !value ? [] : [value, value + value, value + value + value];
+        let result;
 
-class CustomOptionDemo extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      data: [],
-      data2: [],
-    };
-  }
+        if (!value || value.indexOf('@') >= 0) {
+            result = [];
+        } else {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+        }
 
-  search = value => {
-    let result;
+        this.setState({
+            data2: result,
+        });
+    }
 
-    if (!value) {
-      result = [];
-    } else {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+    render() {
+        const { data, data2 } = this.state;
+        return (
+            <div>
+                <AutoComplete
+                    placeholder="fe"
+                    className="test-ac"
+                    prefix={<IconSearch />}
+                    showClear
+                    data={data}
+                    style={{
+                        width: 300,
+                    }}
+                    onSearch={this.handleSearch.bind(this)}
+                    onSelect={v => console.log(v)}
+                    {...props}
+                    ref={this.acref}
+                />
+            </div>
+        );
     }
+}
 
-    this.setState({
-      data: result,
-    });
-  };
-  renderOption = item => {
-    return (
-      <>
-        <span
-          style={{
-            color: 'pink',
-          }}
-        >
-          邮箱
-        </span>
-        : <span>{item}</span>
-      </>
-    );
-  };
-  search2 = value => {
-    let result;
-
-    if (!value) {
-      result = [];
-    } else {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => {
-        return {
-          email: `${value}@${domain}`,
-          time: new Date().valueOf(),
-          value: `${value}@${domain}`,
+export const BasicUsage = () => <Demo />;
+
+class CustomOptionDemo extends Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            data: [],
+            data2: [],
         };
-      });
     }
 
-    this.setState({
-      data2: result,
-    });
-  };
-  renderObjectOption = item => {
-    return (
-      <div>
-        <span
-          style={{
-            color: 'pink',
-          }}
-        >
-          邮箱
-        </span>
-        : <span>{item.email}</span>
-        <span>time</span>: <span>{item.time}</span>
-      </div>
-    );
-  };
+    search = value => {
+        let result;
 
-  render() {
-    return (
-      <>
-        <AutoComplete
-          showClear
-          data={this.state.data}
-          renderItem={this.renderOption}
-          style={{
-            width: '250px',
-          }}
-          onSearch={this.search}
-        ></AutoComplete>
-        <br />
-        <br />
-        <AutoComplete
-          onChangeWithObject
-          style={{
-            width: '250px',
-          }}
-          renderItem={this.renderObjectOption}
-          renderSelectedItem={node => node.email}
-          data={this.state.data2}
-          onSearch={this.search2}
-        />
-      </>
-    );
-  }
+        if (!value) {
+            result = [];
+        } else {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+        }
+
+        this.setState({
+            data: result,
+        });
+    };
+    renderOption = item => {
+        return (
+            <>
+                <span
+                    style={{
+                        color: 'pink',
+                    }}
+                >
+                    邮箱
+                </span>
+                : <span>{item}</span>
+            </>
+        );
+    };
+    search2 = value => {
+        let result;
+
+        if (!value) {
+            result = [];
+        } else {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => {
+                return {
+                    email: `${value}@${domain}`,
+                    time: new Date().valueOf(),
+                    value: `${value}@${domain}`,
+                };
+            });
+        }
+
+        this.setState({
+            data2: result,
+        });
+    };
+    renderObjectOption = item => {
+        return (
+            <div>
+                <span
+                    style={{
+                        color: 'pink',
+                    }}
+                >
+                    邮箱
+                </span>
+                : <span>{item.email}</span>
+                <span>time</span>: <span>{item.time}</span>
+            </div>
+        );
+    };
+
+    render() {
+        return (
+            <>
+                <AutoComplete
+                    showClear
+                    data={this.state.data}
+                    renderItem={this.renderOption}
+                    style={{
+                        width: '250px',
+                    }}
+                    onSearch={this.search}
+                ></AutoComplete>
+                <br />
+                <br />
+                <AutoComplete
+                    onChangeWithObject
+                    style={{
+                        width: '250px',
+                    }}
+                    renderItem={this.renderObjectOption}
+                    renderSelectedItem={node => node.email}
+                    data={this.state.data2}
+                    onSearch={this.search2}
+                />
+            </>
+        );
+    }
 }
 
 export const RenderItem = () => <CustomOptionDemo />;
 
 class WithDefaultValue extends React.Component {
-  constructor() {
-    super();
-    this.state = {
-      data: ['[email protected]', '[email protected]', '[email protected]'],
-    };
-    this.onSearch = this.onSearch.bind(this);
-  }
-  onSearch(value) {
-    let result;
-
-    if (!value) {
-      result = [];
-    } else {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+    constructor() {
+        super();
+        this.state = {
+            data: ['[email protected]', '[email protected]', '[email protected]'],
+        };
+        this.onSearch = this.onSearch.bind(this);
+    }
+    onSearch(value) {
+        let result;
+
+        if (!value) {
+            result = [];
+        } else {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+        }
+
+        this.setState({
+            data: result,
+        });
     }
 
-    this.setState({
-      data: result,
-    });
-  }
-
-  render() {
-    let { data } = this.state;
-    return (
-      <>
-        {/* <AutoComplete
+    render() {
+        let { data } = this.state;
+        return (
+            <>
+                {/* <AutoComplete
            defaultValue='[email protected]'
            data={data}
            onSearch={this.onSearch}
         /> */}
 
-        <AutoComplete defaultValue="semi" data={data} onSearch={this.onSearch} />
-      </>
-    );
-  }
+                <AutoComplete defaultValue="semi" data={data} onSearch={this.onSearch} />
+            </>
+        );
+    }
 }
 
 export const DefaultValue = () => <WithDefaultValue />;
 
 class ControlledMode extends React.Component {
-  constructor() {
-    super();
-    this.state = {
-      data: [],
-      dataObject: [],
-      value: '',
-    };
-    this.onSearch = this.onSearch.bind(this);
-    this.onChange = this.onChange.bind(this);
-  }
-
-  onSearch(value) {
-    let result, resultObject;
-
-    if (!value) {
-      result = [];
-      resultObject = [];
-    } else {
-      result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
-      resultObject = ['gmail.com', '163.com', 'qq.com'].map(domain => ({
-        label: `${value}@${domain}`,
-        value: `${value}@${domain}`,
-      }));
+    constructor() {
+        super();
+        this.state = {
+            data: [],
+            dataObject: [],
+            value: '',
+        };
+        this.onSearch = this.onSearch.bind(this);
+        this.onChange = this.onChange.bind(this);
     }
 
-    this.setState({
-      data: result,
-      dataObject: resultObject,
-    });
-  }
+    onSearch(value) {
+        let result, resultObject;
+
+        if (!value) {
+            result = [];
+            resultObject = [];
+        } else {
+            result = ['gmail.com', '163.com', 'qq.com'].map(domain => `${value}@${domain}`);
+            resultObject = ['gmail.com', '163.com', 'qq.com'].map(domain => ({
+                label: `${value}@${domain}`,
+                value: `${value}@${domain}`,
+            }));
+        }
+
+        this.setState({
+            data: result,
+            dataObject: resultObject,
+        });
+    }
 
-  onChange(value) {
-    this.setState({
-      value: value,
-    });
-  }
+    onChange(value) {
+        this.setState({
+            value: value,
+        });
+    }
 
-  render() {
-    let { data, value, dataObject } = this.state;
-    return (
-      <>
-        <AutoComplete
-          showClear
-          value={value}
-          data={data}
-          onChange={this.onChange}
-          onSearch={this.onSearch}
-          style={{
-            width: 200,
-          }}
-        />
-        <br />
-        <AutoComplete
-          showClear
-          value={value}
-          data={dataObject}
-          onChange={this.onChange}
-          onSearch={this.onSearch}
-          style={{
-            width: 200,
-          }}
-        />
-        <br />
-        <AutoComplete
-          defaultValue={'hello semi'}
-          showClear
-          value={value}
-          data={dataObject}
-          onChange={this.onChange}
-          onSearch={this.onSearch}
-          style={{
-            width: 200,
-          }}
-        />
-      </>
-    );
-  }
+    render() {
+        let { data, value, dataObject } = this.state;
+        return (
+            <>
+                <AutoComplete
+                    showClear
+                    value={value}
+                    data={data}
+                    onChange={this.onChange}
+                    onSearch={this.onSearch}
+                    style={{
+                        width: 200,
+                    }}
+                />
+                <br />
+                <AutoComplete
+                    showClear
+                    value={value}
+                    data={dataObject}
+                    onChange={this.onChange}
+                    onSearch={this.onSearch}
+                    style={{
+                        width: 200,
+                    }}
+                />
+                <br />
+                <AutoComplete
+                    defaultValue={'hello semi'}
+                    showClear
+                    value={value}
+                    data={dataObject}
+                    onChange={this.onChange}
+                    onSearch={this.onSearch}
+                    style={{
+                        width: 200,
+                    }}
+                />
+            </>
+        );
+    }
 }
 
 export const EmptyContent = () => {
-  let [data, setData] = useState([]);
-  const [loading, setLoading] = useState(false);
-
-  const fetchData = v => {
-    setLoading(true);
-    setTimeout(() => {
-      if (!v) {
-        setData([]);
-        setLoading(false);
-        return;
-      }
-
-      setData(() => {
-        const res = Array.from(Array(5)).map(c => Math.random());
-        return res;
-      });
-      setLoading(false);
-    }, 1000);
-  };
-
-  return (
-    <AutoComplete loading={loading} data={data} emptyContent={'空数据'} onSearch={fetchData} />
-  );
+    let [data, setData] = useState([]);
+    const [loading, setLoading] = useState(false);
+
+    const fetchData = v => {
+        setLoading(true);
+        setTimeout(() => {
+            if (!v) {
+                setData([]);
+                setLoading(false);
+                return;
+            }
+
+            setData(() => {
+                const res = Array.from(Array(5)).map(c => Math.random());
+                return res;
+            });
+            setLoading(false);
+        }, 1000);
+    };
+
+    return <AutoComplete loading={loading} data={data} emptyContent={'空数据'} onSearch={fetchData} />;
 };
 
 export const AutoFocus = () => {
-  return <AutoComplete autoFocus />;
+    return <AutoComplete autoFocus />;
 };
 
 export const ControlledValue = () => <ControlledMode />;
@@ -347,4 +346,28 @@ export const CustomTriggerDemo = () => <CustomTrigger />;
 
 export const Disabled = () => <AutoComplete disabled />;
 
-export const KeyDown = () => <AutoComplete onKeyDown={(e) => { console.log('onKeyDown', e.keyCode) }} />;
+export const KeyDown = () => (
+    <AutoComplete
+        onKeyDown={e => {
+            console.log('onKeyDown', e.keyCode);
+        }}
+    />
+);
+
+export const ControlledOnSelectWithObjectDemo = () => {
+    const [v, setV] = useState();
+    return (
+        <Form>
+            <AutoComplete
+                onSelectWithObject
+                data={[]}
+                showClear
+                value={v}
+                placeholder='controlled autocomplete'
+                onChange={val => setV(val)}
+                style={{ width: 200 }}
+            />
+            <Form.AutoComplete field='test' placeholder='form autocomplete' onSelectWithObject data={[]} showClear style={{ width: 200 }} />
+        </Form>
+    );
+};

+ 2 - 2
packages/semi-ui/cascader/__test__/cascader.test.js

@@ -1257,7 +1257,7 @@ describe('Cascader', () => {
         const args = firstCall.args[0]; 
         /* check arguments of triggerRender */
         expect(args.value.size).toEqual(1);
-        expect(args.value).toEqual(new Set('0'));
+        expect(args.value).toEqual(new Set(['Asia']));
         cascaderAutoMerge.unmount();
 
         const spyTriggerRender2 = sinon.spy(() => <span>123</span>);
@@ -1272,7 +1272,7 @@ describe('Cascader', () => {
         const args2 = firstCall2.args[0]; 
         /* check arguments of triggerRender */
         expect(args2.value.size).toEqual(4);
-        expect(args2.value).toEqual(new Set(['0','0-0','0-0-1','0-0-0']));
+        expect(args2.value).toEqual(new Set(["Asia","Asia_SEMI_CASCADER_SPLIT_China","Asia_SEMI_CASCADER_SPLIT_China_SEMI_CASCADER_SPLIT_Beijing","Asia_SEMI_CASCADER_SPLIT_China_SEMI_CASCADER_SPLIT_Shanghai"]));
         cascaderNoAutoMerge.unmount();
     });
 

+ 81 - 1
packages/semi-ui/cascader/_story/cascader.stories.jsx

@@ -1626,7 +1626,7 @@ export const DynamicTreeData = () => {
 
 
 export const SuperLongList = () => {
-    let treeData = new Array(100).fill().map(() => ({ label: '浙江省', value: 'zhejiang' }));
+    let treeData = new Array(100).fill().map((item, index) => ({ label: `浙江省${index}`, value: `zhejiang${index}` }));
     treeData.push({ label: '到底啦', value: 'bottom' })
     return (
         <Cascader
@@ -2184,3 +2184,83 @@ export const VirtualizeInSearch = () => {
       />
   );
 };
+
+function generateOptions(arr, level, frontKey) {
+  const realLevel = level ?? 0;
+  const notLeaf = realLevel !== arr.length - 1;
+  const realFrontKey = frontKey ? `${frontKey}-` : '';
+  return new Array(arr[realLevel])
+    .fill(0)
+    .map((_item, index) => {
+      const data = {
+        label: `label-${realFrontKey}${index}`,
+        value: `value-${realFrontKey}${index}`,
+      };
+      if (notLeaf) {
+        data.children = generateOptions(
+          arr,
+          realLevel + 1,
+          `${realFrontKey}${index}`,
+        );
+      }
+      return data;
+    });
+}
+
+export const LeafOnlyPF = () => {
+  const treeData = useMemo(() => {
+    return generateOptions([4, 10, 10, 10]);
+  }, []);
+
+  return (
+    <Cascader
+      multiple
+      leafOnly
+      maxTagCount={4}
+      treeData={treeData}
+      style={{ width: 200 }}
+    />
+  );
+};
+
+export const SearchPF = () => {
+  const treeData = useMemo(() => {
+    return generateOptions([4, 10, 10, 10]);
+  }, []);
+
+  return (
+    <Cascader
+      filterTreeNode
+      multiple
+      leafOnly
+      maxTagCount={4}
+      treeData={treeData}
+      style={{ width: 200 }}
+    />
+  );
+};
+
+export const ControlledPF = () => {
+  const [cValue, setCValue] = useState([]);
+  const onCascaderChange = useCallback(value => {
+    // console.log('cValue', value);
+    setCValue(value);
+  }, []);
+
+  const treeData = useMemo(() => {
+    return generateOptions([4, 10, 10, 10, 10]);
+  }, []);
+
+  return (
+    <Cascader
+      value={cValue}
+      onChange={onCascaderChange}
+      filterTreeNode
+      leafOnly
+      multiple
+      maxTagCount={4}
+      treeData={treeData}
+      style={{ width: 200 }}
+    />
+  )
+}

+ 17 - 29
packages/semi-ui/cascader/index.tsx

@@ -14,10 +14,10 @@ import CascaderFoundation, {
 } from '@douyinfe/semi-foundation/cascader/foundation';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';
 import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
-import { isSet, isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten } from 'lodash';
+import { isSet, isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten, isObject } from 'lodash';
 import '@douyinfe/semi-foundation/cascader/cascader.scss';
 import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';
-import { findKeysForValues, convertDataToEntities, calcMergeType } from '@douyinfe/semi-foundation/cascader/util';
+import { convertDataToEntities, calcMergeType, getKeyByValuePath } from '@douyinfe/semi-foundation/cascader/util';
 import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/tree/treeUtil';
 import ConfigContext, { ContextValue } from '../configProvider/context';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
@@ -424,31 +424,27 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             return firstInProps || treeDataHasChange;
         };
         const getRealKeys = (realValue: Value, keyEntities: Entities) => {
-            // normallizedValue is used to save the value in two-dimensional array format
-            let normallizedValue: SimpleValueType[][] = [];
+            // normalizedValue is used to save the value in two-dimensional array format
+            let normalizedValue: SimpleValueType[][] = [];
             if (Array.isArray(realValue)) {
-                normallizedValue = Array.isArray(realValue[0])
+                normalizedValue = Array.isArray(realValue[0])
                     ? (realValue as SimpleValueType[][])
                     : ([realValue] as SimpleValueType[][]);
             } else {
                 if (realValue !== undefined) {
-                    normallizedValue = [[realValue]];
+                    normalizedValue = [[realValue]];
                 }
             }
             // formatValuePath is used to save value of valuePath
             const formatValuePath: (string | number)[][] = [];
-            normallizedValue.forEach((valueItem: SimpleValueType[]) => {
-                const formatItem: (string | number)[] = onChangeWithObject ?
+            normalizedValue.forEach((valueItem: SimpleValueType[]) => {
+                const formatItem: (string | number)[] = onChangeWithObject && isObject(valueItem[0]) ?
                     (valueItem as CascaderData[]).map(i => i?.value) :
                     valueItem as (string | number)[];
                 formatValuePath.push(formatItem);
             });
             // formatKeys is used to save key of value
-            const formatKeys: any[] = [];
-            formatValuePath.forEach(v => {
-                const formatKeyItem = findKeysForValues(v, keyEntities);
-                !isEmpty(formatKeyItem) && formatKeys.push(formatKeyItem);
-            });
+            const formatKeys = formatValuePath.map(v => getKeyByValuePath(v));
             return formatKeys;
         };
         const needUpdateTreeData = needUpdate('treeData') || needUpdateData();
@@ -478,7 +474,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
                 if (isSet(realKeys)) {
                     realKeys = [...realKeys];
                 }
-                const calRes = calcCheckedKeys(flatten(realKeys), keyEntities);
+                const calRes = calcCheckedKeys(realKeys, keyEntities);
                 const checkedKeys = new Set(calRes.checkedKeys);
                 const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
                 // disableStrictly
@@ -505,7 +501,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
 
     componentDidUpdate(prevProps: CascaderProps) {
         let isOptionsChanged = false;
-        if (!isEqual(prevProps.treeData, this.props.treeData)) {
+        if (!isEqual(prevProps.treeData, this.props.treeData) && !this.props.multiple) {
             isOptionsChanged = true;
             this.foundation.collectOptions();
         }
@@ -527,13 +523,12 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         this.handleTagRemove(null, keyEntities[key].valuePath);
     }
 
-    renderTagItem = (value: string | Array<string>, idx: number, type: string) => {
+    renderTagItem = (nodeKey: string, idx: number) => {
         const { keyEntities, disabledKeys } = this.state;
         const { size, disabled, displayProp, displayRender, disableStrictly } = this.props;
-        const nodeKey = type === strings.IS_VALUE ? findKeysForValues(value, keyEntities)[0] : value;
         const isDsiabled =
             disabled || keyEntities[nodeKey].data.disabled || (disableStrictly && disabledKeys.has(nodeKey));
-        if (!isEmpty(keyEntities) && !isEmpty(keyEntities[nodeKey])) {
+        if (keyEntities[nodeKey]) {
             const tagCls = cls(`${prefixcls}-selection-tag`, {
                 [`${prefixcls}-selection-tag-disabled`]: isDsiabled,
             });
@@ -567,25 +562,18 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         const { size, disabled, placeholder, maxTagCount, showRestTagsPopover, restTagsPopoverProps } = this.props;
         const { inputValue, checkedKeys, keyEntities, resolvedCheckedKeys } = this.state;
         const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
-        const tagValue: Array<Array<string>> = [];
         const realKeys = this.mergeType === strings.NONE_MERGE_TYPE ? checkedKeys : resolvedCheckedKeys;
-        [...realKeys].forEach(checkedKey => {
-            if (!isEmpty(keyEntities[checkedKey])) {
-                tagValue.push(keyEntities[checkedKey].valuePath);
-            }
-        });
         return (
             <TagInput
                 className={tagInputcls}
                 ref={this.inputRef as any}
                 disabled={disabled}
                 size={size}
-                // TODO Modify logic, not modify type
-                value={(tagValue as unknown) as string[]}
+                value={[...realKeys]}
                 showRestTagsPopover={showRestTagsPopover}
                 restTagsPopoverProps={restTagsPopoverProps}
                 maxTagCount={maxTagCount}
-                renderTagItem={(value, index) => this.renderTagItem(value, index, strings.IS_VALUE)}
+                renderTagItem={(value, index) => this.renderTagItem(value, index)}
                 inputValue={inputValue}
                 onInputChange={this.handleInputChange}
                 // TODO Modify logic, not modify type
@@ -747,7 +735,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         const hiddenTag: Array<ReactNode> = [];
         [...realKeys].forEach((checkedKey, idx) => {
             const notExceedMaxTagCount = !isNumber(maxTagCount) || maxTagCount >= idx + 1;
-            const item = this.renderTagItem(checkedKey, idx, strings.IS_KEY);
+            const item = this.renderTagItem(checkedKey, idx);
             if (notExceedMaxTagCount) {
                 displayTag.push(item);
             } else {
@@ -795,7 +783,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
 
         if (!searchable) {
             if (multiple) {
-                if (isEmpty(checkedKeys)) {
+                if (checkedKeys.size === 0) {
                     return <span className={`${prefixcls}-selection-placeholder`}>{placeholder}</span>;
                 }
                 return this.renderMultipleTags();

+ 1 - 1
packages/semi-ui/dropdown/dropdownItem.tsx

@@ -126,7 +126,7 @@ class DropdownItem extends BaseComponent<DropdownItemProps> {
         }
         return (
             <li role="menuitem" tabIndex={-1} aria-disabled={disabled} {...events} onKeyDown={onKeyDown}
-                ref={ref => forwardRef(ref)} className={itemclass} style={style}>
+                ref={ref => forwardRef(ref)} className={itemclass} style={style} {...this.getDataAttr(this.props)}>
                 {tick}
                 {iconContent}
                 {children}

+ 2 - 1
packages/semi-ui/table/_story/table.stories.jsx

@@ -106,7 +106,8 @@ export {
     FixedPagination,
     ShowHeader,
     KeepDOM,
-    SortIcon
+    SortIcon,
+    FixedAllDisabledAndSelected
 } from './v2';
 export { default as FixSelectAll325 } from './Demos/rowSelection';
 

+ 65 - 0
packages/semi-ui/table/_story/v2/FixedAllDisabledAndSelected/index.tsx

@@ -0,0 +1,65 @@
+import React, { useEffect, useState } from 'react';
+import { Table } from '@douyinfe/semi-ui';
+
+export default function App() {
+    const [selectAll, setSelectAll] = useState(false);
+    const [clueIds, setClueIds] = useState([]);
+    const columns = [
+        {
+            title: '排行',
+            dataIndex: 'index',
+        },
+        {
+            title: 'name',
+            dataIndex: 'name',
+        },
+    ];
+    const data = Array(20)
+        .fill(0)
+        .map((_, index) => {
+            return {
+                key: index,
+                name: `name ${index}`,
+                index: index,
+                clue_id: index,
+            };
+        });
+    const rowSelection = {
+        disabled: selectAll,
+        getCheckboxProps: record => {
+            if (selectAll) {
+                return {
+                    disabled: true,
+                };
+            }
+            return {};
+        },
+
+        onChange: (selectedRowKeys, selectedRows) => {
+            console.log(`select all onChange: ${selectedRowKeys}`, selectedRows);
+            console.log(selectedRowKeys);
+            setClueIds(selectedRows.map(row => row.clue_id));
+        },
+        selectedRowKeys: clueIds,
+    };
+    useEffect(() => {
+        if (selectAll) {
+            let newIds = data.map(row => row.clue_id);
+            console.log(newIds);
+            setClueIds(newIds);
+        }
+    }, [selectAll]);
+
+    return (
+        <div>
+            <button
+                onClick={() => {
+                    setSelectAll(true);
+                }}
+            >
+                点击全选
+            </button>
+            <Table columns={columns} dataSource={data} rowSelection={rowSelection} />
+        </div>
+    );
+}

+ 1 - 0
packages/semi-ui/table/_story/v2/index.js

@@ -27,3 +27,4 @@ export { default as FixedPagination } from './FixedPagination';
 export { default as ShowHeader } from './ShowHeader';
 export { default as KeepDOM } from './KeepDOM';
 export { default as SortIcon } from './SortIcon';
+export { default as FixedAllDisabledAndSelected } from './FixedAllDisabledAndSelected';