Explorar o código

Merge branch 'milestone-2.5' of https://github.com/DouyinFE/semi-design into release

chenyuling %!s(int64=3) %!d(string=hai) anos
pai
achega
77b543304e

+ 4 - 4
.github/workflows/cypress.yml

@@ -66,8 +66,8 @@ jobs:
         with:
           # we have already installed all dependencies above
           install: true
-          start: npx http-server -p 6009 storybook-static
-          wait-on: 'http://localhost:6009'
+          start: npx http-server -p 6006 storybook-static
+          wait-on: 'http://localhost:6006'
           wait-on-timeout: 120
           browser: chrome
           record: true
@@ -103,8 +103,8 @@ jobs:
         with:
           # we have already installed all dependencies above
           install: true
-          start: npx http-server -p 6009 storybook-static
-          wait-on: 'http://localhost:6009'
+          start: npx http-server -p 6006 storybook-static
+          wait-on: 'http://localhost:6006'
           wait-on-timeout: 120
           browser: firefox
           record: true

+ 132 - 0
content/input/upload/index-en-US.md

@@ -484,6 +484,41 @@ import { IconUpload, IconFile } from '@douyinfe/semi-icons';
 };
 ```
 
+### Custom list operation area
+
+When `listType` is `list`, you can customize the list operation area by passing in `renderFileOperation`
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload, Button } from '@douyinfe/semi-ui';
+import { IconUpload, IconDownload, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'vigo.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url: 'https://sf6-cdn-tos.douyinstatic.com/img/ee-finolhu/c2a65140483e4a20802d64af5fec1b39~noop.image',
+        }
+    ];
+    const renderFileOperation = (fileItem) => (
+        <div style={{display: 'flex',columnGap: 8, padding: '0 8px'}}>
+            <Button icon={<IconEyeOpened></IconEyeOpened>} type="tertiary" theme="borderless" size="small"></Button>
+            <Button icon={<IconDownload></IconDownload>} type="tertiary" theme="borderless" size="small"></Button>
+            <Button onClick={e=>fileItem.onRemove()} icon={<IconDelete></IconDelete>} type="tertiary" theme="borderless" size="small"></Button>
+        </div>
+    )
+    return <Upload action={action} defaultFileList={defaultFileList} itemStyle={{width: 300}} renderFileOperation={renderFileOperation}>
+            <Button icon={<IconUpload />} theme="light">Upload</Button>
+        </Upload>
+    }
+```
+
 ### Default file list
 
 The uploaded files can be displayed through `defaultFileList`. When you need to preview the thumbnail of the default file, you can set the `preview` attribute of the corresponding `item` in `defaultFileList` to `true`
@@ -653,6 +688,100 @@ import { IconPlus } from '@douyinfe/semi-icons';
 };
 ```
 
+You can customize the preview icon through `renderPicPreviewIcon`, `onPreviewClick`, when the replacement icon `showReplace` is displayed, the preview icon will no longer be displayed. <br />
+When you need to customize the preview/replacement function, you need to turn off the replacement function and use `renderPicPreviewIcon` to listen for icon click events. <br />
+`onPreviewClick` listens for the click event of the single image container
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload } from '@douyinfe/semi-ui';
+import { IconPlus, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    const handlePreview = (file) => {
+        const feature = "width=300,height=300";
+        window.open(file.url, 'imagePreview', feature);
+    }
+    return (
+        <>
+            <Upload
+                action={action}
+                listType="picture"
+                showPicInfo
+                accept="image/*"
+                multiple
+                defaultFileList={defaultFileList}
+                onPreviewClick={handlePreview}
+                renderPicPreviewIcon={()=><IconEyeOpened style={{color: 'var(--semi-color-white)', fontSize: 24}} />}
+            >
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
+Set `hotSpotLocation` to customize the order of click hotspots, the default is at the end of the photo wall list
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload, Select } from '@douyinfe/semi-ui';
+import { IconPlus, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    const handlePreview = (file) => {
+        const feature = "width=300,height=300";
+        window.open(file.url, 'imagePreview', feature);
+    };
+    const [hotSpotLocation, $hotSpotLocation] = useState('end');
+    return (
+        <>
+            <Select value={hotSpotLocation} onChange={$hotSpotLocation} style={{ width: 120 }}>
+                <Select.Option value='start'>Start</Select.Option>
+                <Select.Option value='end'>End</Select.Option>
+            </Select>
+            <hr />
+            <Upload
+                action={action}
+                listType="picture"
+                showPicInfo
+                accept="image/*"
+                multiple
+                hotSpotLocation={hotSpotLocation}
+                defaultFileList={defaultFileList}
+                onPreviewClick={handlePreview}
+            >
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
 ### Disabled
 
 ```jsx live=true width=48%
@@ -1102,6 +1231,7 @@ The Upload component is an interactive control that can trigger file selection w
 |fileList | A list of uploaded files. When this value is passed in, upload is a controlled component | Array<FileItem\> | | 1.0.0 |
 |fileName | has the same function as name and is mainly used in Form.Upload. In order to avoid conflicts with the props.name of Field, a renamed props is provided here | string | | 1.0.0 |
 |headers | The headers attached to the upload or the method to return the uploaded additional headers| object\|(file: [File](https://developer.mozilla.org/zh-CN/docs/Web/API/File)) = > object | {} | |
+|hotSpotLocation | 照片墙点击热区的放置位置,可选值 `start`, `end` | string | 'end' | 2.5.0 |
 |itemStyle | Inline style of fileCard | CSSProperties | | 1.0.0 |
 |limit | Maximum number of files allowed to be uploaded | number | | |
 |listType | File list display type, optional `picture`, `list` | string |'list' | |
@@ -1127,7 +1257,9 @@ The Upload component is an interactive control that can trigger file selection w
 |prompt | Custom slot, which can be used to insert prompt text. Different from writing directly in `children`, the content of `prompt` will not trigger upload when clicked.<br/>(In the picture wall mode, the incoming prompt is only supported after v1.3.0) | ReactNode | | |
 |promptPosition | The position of the prompt text. When the listType is list, the reference object is the children element; when the listType is picture, the reference object is the picture list. Optional values ​​`left`, `right`, `bottom`<br/> (In picture wall mode, promptPosition is only supported after v1.3.0) | string |'right' | |
 |renderFileItem | Custom rendering of fileCard | (renderProps: RenderFileItemProps) => ReactNode | | 1.0.0 |
+|renderFileOperation | Custom list item operation area | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderPicInfo| Custom photo wall information, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
+|renderPicPreviewIcon| The preview icon displayed when customizing the photo wall hover, only valid in photo wall mode | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderThumbnail| Custom picture wall thumb, only valid in photo wall mode| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |showClear | When limit is not 1 and the current number of uploaded files is greater than 1, whether to show the clear button | boolean | true | 1.0.0 |
 |showPicInfo| Whether to display picture information, only valid in photo wall mode | boolean| false | 2.2.0 |

+ 133 - 0
content/input/upload/index.md

@@ -488,6 +488,41 @@ import { IconUpload, IconFile } from '@douyinfe/semi-icons';
 };
 ```
 
+### 自定义列表操作区
+
+`listType` 为 `list` 时,可以通过传入 `renderFileOperation` 来自定义列表操作区
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload, Button } from '@douyinfe/semi-ui';
+import { IconUpload, IconDownload, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'vigo.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url: 'https://sf6-cdn-tos.douyinstatic.com/img/ee-finolhu/c2a65140483e4a20802d64af5fec1b39~noop.image',
+        }
+    ];
+    const renderFileOperation = (fileItem) => (
+        <div style={{display: 'flex',columnGap: 8, padding: '0 8px'}}>
+            <Button icon={<IconEyeOpened></IconEyeOpened>} type="tertiary" theme="borderless" size="small"></Button>
+            <Button icon={<IconDownload></IconDownload>} type="tertiary" theme="borderless" size="small"></Button>
+            <Button onClick={e=>fileItem.onRemove()} icon={<IconDelete></IconDelete>} type="tertiary" theme="borderless" size="small"></Button>
+        </div>
+    )
+    return <Upload action={action} defaultFileList={defaultFileList} itemStyle={{width: 300}} renderFileOperation={renderFileOperation}>
+            <Button icon={<IconUpload />} theme="light">点击上传</Button>
+        </Upload>
+    }
+```
+
 ### 默认文件列表
 
 通过 `defaultFileList` 可以展示已上传的文件。当需要预览默认文件的缩略图时,你可以将 `defaultFileList` 内对应 `item` 的 `preview` 属性设为 `true`
@@ -665,6 +700,101 @@ import { IconPlus } from '@douyinfe/semi-icons';
 };
 ```
 
+可以通过 `renderPicPreviewIcon`,`onPreviewClick` 来自定义预览图标,当显示替换图标 `showReplace` 时,不会再显示预览图标<br />
+当需要自定义预览/替换功能时,需要关闭替换功能,使用 `renderPicPreviewIcon` 监听图标点击事件即可。<br />
+`onPreviewClick` 监听的是单张图片容器的点击事件
+
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload } from '@douyinfe/semi-ui';
+import { IconPlus, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    const handlePreview = (file) => {
+        const feature = "width=300,height=300";
+        window.open(file.url, 'imagePreview', feature);
+    }
+    return (
+        <>
+            <Upload
+                action={action}
+                listType="picture"
+                showPicInfo
+                accept="image/*"
+                multiple
+                defaultFileList={defaultFileList}
+                onPreviewClick={handlePreview}
+                renderPicPreviewIcon={()=><IconEyeOpened style={{color: 'var(--semi-color-white)', fontSize: 24}} />}
+            >
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
+设置 `hotSpotLocation` 自定义点击热区的顺序,默认在照片墙列表结尾
+
+```jsx live=true width=48%
+import React from 'react';
+import { Upload, Select } from '@douyinfe/semi-ui';
+import { IconPlus, IconEyeOpened } from '@douyinfe/semi-icons';
+
+() => {
+    let action = 'https://run.mocky.io/v3/d6ac5c9e-4d39-4309-a747-7ed3b5694859';
+    const defaultFileList = [
+        {
+            uid: '1',
+            name: 'jiafang.png',
+            status: 'success',
+            size: '130KB',
+            preview: true,
+            url:
+                'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/e82f3b261133d2b20d85e8483c203112.jpg',
+        },
+    ];
+    const handlePreview = (file) => {
+        const feature = "width=300,height=300";
+        window.open(file.url, 'imagePreview', feature);
+    };
+    const [hotSpotLocation, $hotSpotLocation] = useState('end');
+    return (
+        <>
+            <Select value={hotSpotLocation} onChange={$hotSpotLocation} style={{ width: 120 }}>
+                <Select.Option value='start'>开始</Select.Option>
+                <Select.Option value='end'>结尾</Select.Option>
+            </Select>
+            <hr />
+            <Upload
+                action={action}
+                listType="picture"
+                showPicInfo
+                accept="image/*"
+                multiple
+                hotSpotLocation={hotSpotLocation}
+                defaultFileList={defaultFileList}
+                onPreviewClick={handlePreview}
+            >
+                <IconPlus size="extra-large" />
+            </Upload>
+        </>
+    );
+};
+```
+
 ### 禁用
 
 ```jsx live=true width=48%
@@ -1115,6 +1245,7 @@ Upload组件是一个可交互的控件,在点击或拖拽时触发文件选
 |fileList | 已上传的文件列表,传入该值时,upload 即为受控组件 | Array<FileItem\> |  | 1.0.0 |
 |fileName | 作用与 name 相同,主要在 Form.Upload 中使用,为了避免与 Field 的 props.name 冲突,此处另外提供一个重命名的 props | string |  | 1.0.0 |
 |headers | 上传时附带的 headers 或返回上传额外 headers 的方法 | object\|(file: [File](https://developer.mozilla.org/zh-CN/docs/Web/API/File)) => object | {} |  |
+|hotSpotLocation | 照片墙点击热区的放置位置,可选值 `start`, `end` | string | 'end' | 2.5.0 |
 |itemStyle | fileCard 的内联样式 | CSSProperties |  | 1.0.0 |
 |limit | 最大允许上传文件个数 | number |  |  |
 |listType | 文件列表展示类型,可选`picture`、`list` | string | 'list' |  |
@@ -1140,7 +1271,9 @@ Upload组件是一个可交互的控件,在点击或拖拽时触发文件选
 |prompt | 自定义插槽,可用于插入提示文本。与直接在 `children` 中写的区别时,`prompt` 的内容在点击时不会触发上传<br/>(图片墙模式下,v1.3.0 后才支持传入 prompt) | ReactNode |  |  |
 |promptPosition | 提示文本的位置,当 listType 为 list 时,参照物为 children 元素;当 listType 为 picture 时,参照物为图片列表。可选值 `left`、`right`、`bottom`<br/>(图片墙模式下,v1.3.0 后才支持使用 promptPosition) | string | 'right' |  |
 |renderFileItem | fileCard 的自定义渲染 | (renderProps: RenderFileItemProps) => ReactNode |  | 1.0.0 |
+|renderFileOperation | 自定义列表项操作区 | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderPicInfo| 自定义照片墙信息,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
+|renderPicPreviewIcon| 自定义照片墙hover时展示的预览图标,只在照片墙模式下有效 | (renderProps: RenderFileItemProps)=>ReactNode | | 2.5.0 |
 |renderThumbnail| 自定义图片墙缩略图,只在照片墙模式下有效| (renderProps: RenderFileItemProps)=>ReactNode | | 2.2.0 |
 |showClear | 在 limit 不为 1 且当前已上传文件数大于 1 时,是否展示清空按钮 | boolean | true | 1.0.0 |
 |showPicInfo| 是否显示图片信息,只在照片墙模式下有效| boolean| false | 2.2.0 |

+ 1 - 0
content/show/table/index-en-US.md

@@ -4539,6 +4539,7 @@ import { Table } from '@douyinfe/semi-ui';
 | className                     | Column style name                                                                                                                                                                                    | string                                                                                             |         |
 | colSpan                       | When header columns merge, set to 0, do not render                                                                                                                                                   | number                                                                                             |         |
 | dataIndex                     | The key corresponding to the column data in the data item. It is required when using sorter or filter.                                                                                                                                           | string                                                                                             |         |
+| defaultFilteredValue          | Default value of the filter, the filter state of the external control column with a value of the screened value array                            | any[]                                                                                           |        | **2.5.0** |
 | defaultSortOrder              | The default value of sortOrder, one of 'ascend'\|'descend'\|false                                         | boolean\| string                                                                                          |  false  | **1.31.0**
 | filterChildrenRecord          | Whether the child data needs to be filtered locally. If this function is enabled, if the child meets the filtering criteria, the parent will retain it even if it does not meet the criteria.        | boolean                                                                                            |         | **0.29.0**                         |
 | filterDropdown                | You can customize the filter menu. This function is only responsible for rendering the layer and needs to write a variety of interactions.                                                           | ReactNode                                                                                          |         |

+ 1 - 0
content/show/table/index.md

@@ -4550,6 +4550,7 @@ import { Table } from '@douyinfe/semi-ui';
 | children                      | 表头合并时用于子列的设置                                                                               | Column[]                                                                                        |        |
 | colSpan                       | 表头列合并,设置为 0 时,不渲染                                                                         | number                                                                                          |        |
 | dataIndex                     | 列数据在数据项中对应的 key,使用排序或筛选时必传                                                       | string                                                                                          |        |
+| defaultFilteredValue          | 筛选的默认值,值为已筛选的 value 数组                                    | any[]                                                                                           |        | **2.5.0** |
 | defaultSortOrder              | 排序的默认值,可设置为 'ascend'\|'descend'\|false                                                      | boolean\| string                                                                                | false  | **1.31.0**                  |
 | filterChildrenRecord          | 是否需要对子级数据进行本地过滤,开启该功能后如果子级符合过滤标准,父级即使不符合仍然会保留             | boolean                                                                                         |        | **0.29.0**                  |
 | filterDropdown                | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互                                         | ReactNode                                                                                       |        |

+ 2 - 1
cypress.json

@@ -1,3 +1,4 @@
 {
-  "projectId": "k83u7j"
+  "projectId": "k83u7j",
+  "scrollBehavior": false
 }

+ 6 - 6
cypress/integration/datePicker.spec.js

@@ -11,7 +11,7 @@
  */
 describe('DatePicker', () => {
     it('dateTime needConfirm cancel', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=1] .semi-input-wrapper').click();
         cy.get('.semi-datepicker-footer > .semi-button-borderless')
             .then(($btn) => {
@@ -22,7 +22,7 @@ describe('DatePicker', () => {
     });
 
     it('dateTime needConfirm confirm', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=1] .semi-input-wrapper').click();
         cy.get('.semi-datepicker-day').contains('15')
             .then($day => {
@@ -36,7 +36,7 @@ describe('DatePicker', () => {
     });
 
     it('dateTime needConfirm select+cancel', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=1] .semi-input-wrapper').click();
         cy.get('.semi-datepicker-day').contains('15')
             .then($day => {
@@ -51,7 +51,7 @@ describe('DatePicker', () => {
     });
 
     it('dateTimeRange needConfirm cancel', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
         cy.get('.semi-datepicker-footer > .semi-button-borderless')
             .then($btn => {
@@ -61,7 +61,7 @@ describe('DatePicker', () => {
     });
 
     it('dateTimeRange needConfirm select+cancel', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
         cy.get('.semi-datepicker-month-grid-left .semi-datepicker-day').contains('15')
             .then($day => {
@@ -80,7 +80,7 @@ describe('DatePicker', () => {
     });
 
     it('dateTimeRange needConfirm confirm', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
         cy.get('.semi-datepicker-month-grid-left .semi-datepicker-day').contains('15')
             .then($day => {

+ 16 - 2
cypress/integration/table.spec.js

@@ -7,7 +7,7 @@
 
 describe('table', () => {
     it('row selection', () => {
-        cy.visit('http://127.0.0.1:6009/iframe.html?id=table--selection-table&args=&viewMode=story');
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=table--selection-table&args=&viewMode=story');
         cy.get('.semi-table-row-head .semi-checkbox-inner-display').click();
         cy.get('.semi-checkbox-checked').should('have.length', 4);
     });
@@ -17,11 +17,25 @@ describe('table', () => {
      * 即更新 columns 不影响 currentPage
      */
     it('columns change ', () => {
-        cy.visit('http://localhost:6009/iframe.html?id=table--fixed-columns-change&viewMode=story');
+        cy.visit('http://localhost:6006/iframe.html?id=table--fixed-columns-change&viewMode=story');
         cy.get('.semi-page-item').contains('2').click();
         cy.get('.semi-table-tbody .semi-checkbox').eq(1).click()
             .then(() => {
                 cy.get('.semi-page-item').contains('2').should('have.class', 'semi-page-item-active');
             });
     });
+
+    it('defaultFilteredValue', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=table--default-filtered-value&args=&viewMode=story');
+        // 筛选为默认值
+        cy.contains('显示第 1 条-第 10 条,共 23 条');
+        // 动态变更数据后,默认筛选生效
+        cy.get('.semi-button').contains('toggle change dataSource (46/25)').click();
+        cy.contains('显示第 1 条-第 10 条,共 12 条');
+        // 筛选手动设置为空后,动态变更数据,筛选值为空
+        cy.get('.semi-table-column-filter').click();
+        cy.get('.semi-dropdown-menu .semi-dropdown-item:nth-child(2)').click();
+        cy.get('.semi-button').contains('toggle change dataSource (46/25)').click();
+        cy.contains('显示第 1 条-第 10 条,共 46 条');
+    });
 });

+ 29 - 0
cypress/integration/tooltip.spec.js

@@ -0,0 +1,29 @@
+// tooltip.spec.js created with Cypress
+//
+// Start writing your Cypress tests below!
+// If you're unfamiliar with how Cypress works,
+// check out the link below and learn how to write your first test:
+// https://on.cypress.io/writing-first-test
+
+/**
+ * Cypress will default scroll element into view
+ * @see https://docs.cypress.io/guides/core-concepts/interacting-with-elements#Scrolling
+ */
+describe('tooltip', () => {
+    it('leftTopOver autoAdjustOverflow', () => {
+        const viewportWidth = 1200;
+        const viewportHeight = 660;
+        const triggerWidth = 200;
+        const triggerHeight = 32;
+        const leftTopPosition = { offset: { top: 0, left: 0 }}; 
+        const rightBottomPosition = { offset: { top: -viewportHeight + triggerHeight, left: -viewportWidth + triggerWidth }};
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=tooltip--left-top-over-auto-adjust-overflow&args=&viewMode=story');
+        cy.viewport(viewportWidth, viewportHeight);
+        const dataSelector = `[data-cy=leftTopOver]`;
+        cy.get(dataSelector).scrollIntoView(leftTopPosition);
+        cy.get(dataSelector).click({force: true});
+        cy.get('[x-placement="leftTopOver"]').should('have.length', 1);
+        cy.get(dataSelector).scrollIntoView(rightBottomPosition);
+        cy.get('[x-placement="rightBottomOver"]').should('have.length', 1);
+    });
+});

+ 1 - 1
package.json

@@ -29,7 +29,7 @@
     "test:story": "cross-env NODE_ENV=test type=story ./node_modules/.bin/jest --silent --notify --maxWorkers=4",
     "test:coverage": "cross-env TZ=Asia/Shanghai NODE_ENV=test type=unit ./node_modules/.bin/jest --silent --coverage --notify",
     "test:storyUpdate": "cross-env NODE_ENV=test type=story ./node_modules/.bin/jest --silent -u --notify --maxWorkers=4",
-    "test:cy": "npx http-server storybook-static -p 6009 && npx wait-on http://127.0.0.1:6009 && cypress open",
+    "test:cy": "npx wait-on http://127.0.0.1:6006 && ./node_modules/.bin/cypress open",
     "build:lib": "lerna run build:lib",
     "build:js": "lerna run build:js",
     "build:css": "lerna run build:css",

+ 23 - 10
packages/semi-foundation/table/foundation.ts

@@ -34,6 +34,7 @@ export interface BaseColumnProps<RecordType> {
     className?: string;
     colSpan?: number;
     dataIndex?: string;
+    defaultFilteredValue?: any[];
     defaultSortOrder?: BaseSortOrder;
     filterChildrenRecord?: boolean;
     filterDropdown?: any;
@@ -200,13 +201,23 @@ class TableFoundation<RecordType> extends BaseFoundation<TableAdapter<RecordType
      */
     getFilteredSortedDataSource(dataSource: RecordType[], queries: BaseColumnProps<RecordType>[]) {
         const filteredDataSource = this.filterDataSource(dataSource, queries.filter(
-            query => (
-                isFunction(query.onFilter) &&
-                Array.isArray(query.filters) &&
-                query.filters.length &&
-                Array.isArray(query.filteredValue) &&
-                query.filteredValue.length
-            )
+            query => {
+                /**
+                 * 这里无需判断 filteredValue 是否为数组,初始化时它是 `undefined`,点击选择空时为 `[]`
+                 * 初始化时我们应该用 `defaultFilteredValue`,点击后我们应该用 `filteredValue`
+                 * 
+                 * There is no need to judge whether `filteredValue` is an array here, because it is `undefined` when initialized, and `[]` when you click to select empty
+                 * When initializing we should use `defaultFilteredValue`, after clicking we should use `filteredValue`
+                 */
+                const currentFilteredValue = query.filteredValue ? query.filteredValue : query.defaultFilteredValue;
+                return (
+                    isFunction(query.onFilter) &&
+                    Array.isArray(query.filters) &&
+                    query.filters.length &&
+                    Array.isArray(currentFilteredValue) &&
+                    currentFilteredValue.length
+                );
+            }
         ));
         const sortedDataSource = this.sortDataSource(filteredDataSource, queries.filter(query => query && isFunction(query.sorter)));
         return sortedDataSource;
@@ -371,8 +382,9 @@ class TableFoundation<RecordType> extends BaseFoundation<TableAdapter<RecordType
         const childrenRecordName = this.getProp('childrenRecordName');
 
         each(filters, filterObj => {
-            const { onFilter, filteredValue, filterChildrenRecord } = filterObj;
-            if (typeof onFilter === 'function' && Array.isArray(filteredValue) && filteredValue.length) {
+            const { onFilter, filteredValue, filterChildrenRecord, defaultFilteredValue } = filterObj;
+            const currentFilteredValue = Array.isArray(filteredValue) ? filteredValue : defaultFilteredValue;
+            if (typeof onFilter === 'function' && Array.isArray(currentFilteredValue) && currentFilteredValue.length) {
                 hasValidFilters = true;
 
                 if (filteredData === null) {
@@ -381,7 +393,7 @@ class TableFoundation<RecordType> extends BaseFoundation<TableAdapter<RecordType
                     dataSource = Array.from(filteredData && filteredData.values());
                     filteredData = new Map();
                 }
-                each(filteredValue, value => {
+                each(currentFilteredValue, value => {
                     each(dataSource, record => {
                         const childrenRecords = get(record, childrenRecordName);
                         const recordKey = this.getRecordKey(record);
@@ -1165,6 +1177,7 @@ export interface BaseChangeInfoFilter<RecordType> {
     filters?: BaseFilter[];
     onFilter?: (filteredValue?: any, record?: RecordType) => boolean;
     filteredValue?: any[];
+    defaultFilteredValue?: any[];
     children?: BaseFilter[];
     filterChildrenRecord?: boolean;
 }

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

@@ -20,6 +20,8 @@ const strings = {
         'bottomRight',
         'leftTopOver',
         'rightTopOver',
+        'leftBottomOver',
+        'rightBottomOver',
     ],
     TRIGGER_SET: ['hover', 'focus', 'click', 'custom'],
     STATUS_DISABLED: 'disabled',

+ 52 - 4
packages/semi-foundation/tooltip/foundation.ts

@@ -434,6 +434,17 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                 top = triggerRect.top;
                 translateX = -1;
                 break;
+            case 'leftBottomOver':
+                left = triggerRect.left;
+                top = triggerRect.bottom;
+                translateY = -1;
+                break;
+            case 'rightBottomOver':
+                left = triggerRect.right;
+                top = triggerRect.bottom;
+                translateX = -1;
+                translateY = -1;
+                break;
             default:
                 break;
         }
@@ -602,14 +613,19 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
 
             const shouldReverseTop = clientTop < wrapperRect.height + spacing && restClientBottom > wrapperRect.height + spacing;
             const shouldReverseLeft = clientLeft < wrapperRect.width + spacing && restClientRight > wrapperRect.width + spacing;
-            const sholdReverseBottom = restClientBottom < wrapperRect.height + spacing && clientTop > wrapperRect.height + spacing;
+            const shouldReverseBottom = restClientBottom < wrapperRect.height + spacing && clientTop > wrapperRect.height + spacing;
             const shouldReverseRight = restClientRight < wrapperRect.width + spacing && clientLeft > wrapperRect.width + spacing;
+            const shouldReverseTopOver = restClientTop < wrapperRect.height + spacing && clientBottom > wrapperRect.height + spacing;
+            const shouldReverseBottomOver = clientBottom < wrapperRect.height + spacing && restClientTop > wrapperRect.height + spacing;
 
             const shouldReverseTopSide = restClientTop < wrapperRect.height && clientBottom > wrapperRect.height;
             const shouldReverseBottomSide = clientBottom < wrapperRect.height && restClientTop > wrapperRect.height;
             const shouldReverseLeftSide = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
             const shouldReverseRightSide = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
 
+            const shouldReverseLeftOver = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
+            const shouldReverseRightOver = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
+
             switch (position) {
                 case 'top':
                     if (shouldReverseTop) {
@@ -654,12 +670,12 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                     }
                     break;
                 case 'bottom':
-                    if (sholdReverseBottom) {
+                    if (shouldReverseBottom) {
                         position = this._reversePos(position, true);
                     }
                     break;
                 case 'bottomLeft':
-                    if (sholdReverseBottom) {
+                    if (shouldReverseBottom) {
                         position = this._reversePos(position, true);
                     }
                     if (shouldReverseLeftSide && widthIsBigger) {
@@ -667,7 +683,7 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                     }
                     break;
                 case 'bottomRight':
-                    if (sholdReverseBottom) {
+                    if (shouldReverseBottom) {
                         position = this._reversePos(position, true);
                     }
                     if (shouldReverseRightSide && widthIsBigger) {
@@ -695,6 +711,38 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                         position = this._reversePos(position, true);
                     }
                     break;
+                case 'leftTopOver':
+                    if (shouldReverseTopOver) {
+                        position = this._reversePos(position, true);
+                    }
+                    if (shouldReverseLeftOver) {
+                        position = this._reversePos(position);
+                    }
+                    break;
+                case 'leftBottomOver':
+                    if (shouldReverseBottomOver) {
+                        position = this._reversePos(position, true);
+                    }
+                    if (shouldReverseLeftOver) {
+                        position = this._reversePos(position);
+                    }
+                    break;
+                case 'rightTopOver':
+                    if (shouldReverseTopOver) {
+                        position = this._reversePos(position, true);
+                    }
+                    if (shouldReverseRightOver) {
+                        position = this._reversePos(position);
+                    }
+                    break;
+                case 'rightBottomOver':
+                    if (shouldReverseBottomOver) {
+                        position = this._reversePos(position, true);
+                    }
+                    if (shouldReverseRightOver) {
+                        position = this._reversePos(position);
+                    }
+                    break;
                 default:
                     break;
             }

+ 66 - 41
packages/semi-foundation/upload/upload.scss

@@ -1,4 +1,4 @@
-@import "./variables.scss";
+@import './variables.scss';
 
 $module: #{$prefix}-upload;
 
@@ -47,7 +47,7 @@ $module: #{$prefix}-upload;
         align-items: center;
     }
 
-    &[x-prompt-pos="right"] {
+    &[x-prompt-pos='right'] {
         .#{$module}-add {
             display: inline-flex;
         }
@@ -57,7 +57,7 @@ $module: #{$prefix}-upload;
         }
     }
 
-    &[x-prompt-pos="bottom"] {
+    &[x-prompt-pos='bottom'] {
         .#{$module}-add {
             display: flex;
         }
@@ -72,15 +72,14 @@ $module: #{$prefix}-upload;
         }
     }
 
-    &[x-prompt-pos="left"] {
+    &[x-prompt-pos='left'] {
         .#{$module}-add {
             display: inline-flex;
-            order: 2;
         }
 
         .#{$module}-prompt {
             display: inline-flex;
-            order: 1;
+            order: -1;
         }
 
         .#{$module}-file-list {
@@ -247,7 +246,6 @@ $module: #{$prefix}-upload;
         }
 
         &-icon {
-
             &-loading,
             &-error {
                 font-size: $width-upload_file_card-icon;
@@ -275,7 +273,6 @@ $module: #{$prefix}-upload;
             }
 
             .#{$module}-file-card {
-
                 &-info-validate-message {
                     color: $color-upload_file_card_fail_info-text;
                 }
@@ -288,26 +285,18 @@ $module: #{$prefix}-upload;
     &-picture {
         display: flex;
 
-        &[x-prompt-pos="bottom"] {
+        &[x-prompt-pos='bottom'] {
             flex-direction: column;
 
             .#{$module}-prompt {
                 order: 1;
             }
-
-            .#{$module}-add {
-                order: 0;
-            }
         }
 
-        &[x-prompt-pos="right"] {
+        &[x-prompt-pos='right'] {
             .#{$module}-prompt {
                 order: 1;
             }
-
-            .#{$module}-add {
-                order: 0;
-            }
         }
 
         &-add {
@@ -321,7 +310,6 @@ $module: #{$prefix}-upload;
             border: $width-upload_picture_add-border dashed $color-upload-border;
             color: $color-upload-icon;
             border-radius: $radius-upload_picture_add;
-            order: 2;
             cursor: pointer;
 
             &:hover {
@@ -370,38 +358,32 @@ $module: #{$prefix}-upload;
             }
 
             &-close {
-                width: $width-upload_picture_file_card_close;
-                height: $width-upload_picture_file_card_close;
-                background-color: $color-upload_pic_remove-bg;
+                visibility: hidden;
+                display: inline-flex;
                 position: absolute;
                 top: $spacing-upload_picture_file_card_close-top;
                 right: $spacing-upload_picture_file_card_close-right;
-                @include all-center;
-                display: none;
                 border-radius: $radius-upload_picture_file_card_close;
-                color: white;
                 cursor: pointer;
                 transition: all 0s;
             }
-
-            &:hover {
-                .#{$module}-picture-file-card-close {
-                    display: flex;
-                }
-
-                .#{$module}-picture-file-card-replace {
-                    visibility: visible;
-                }
+            &-icon-close {
+                font-size: $width-upload_picture_file_card_close;
+                color: $color-upload_picture_file_card_close-icon;
             }
 
-            .#{$prefix}-progress-circle {
+            &::before {
+                visibility: hidden;
+                background-color: $color-upload_picture_file_card_hover-bg;
+                content: '';
                 position: absolute;
-                top: 50%;
-                left: 50%;
-                transform: translate(-50%, -50%);
+                left: 0;
+                right: 0;
+                top: 0;
+                bottom: 0;
             }
-
             &-retry {
+                visibility: hidden;
                 background-color: $color-upload_file_card_retry-bg;
                 width: $width-upload_file_card_retry;
                 height: $width-upload_file_card_retry;
@@ -416,7 +398,6 @@ $module: #{$prefix}-upload;
                 justify-content: center;
                 cursor: pointer;
             }
-
             &-icon-retry {
                 transform: scale(-1, 1);
                 font-size: $width-upload_file_card_retry-icon;
@@ -432,6 +413,17 @@ $module: #{$prefix}-upload;
                 color: $color-upload_replace-text;
                 transform: translate3D(-50%, -50%, 0);
             }
+
+            &-preview {
+                visibility: hidden;
+                display: inline-flex;
+                position: absolute;
+                cursor: pointer;
+                top: 50%;
+                left: 50%;
+                transform: translate3D(-50%, -50%, 0);
+            }
+
             &-pic-info {
                 display: inline-flex;
                 box-sizing: border-box;
@@ -468,6 +460,39 @@ $module: #{$prefix}-upload;
             &-error {
                 outline: 1px solid $color-upload_picture_file_card_error-border;
             }
+
+            &:hover {
+                &::before {
+                    visibility: visible;
+                }
+
+                .#{$module}-picture-file-card-close {
+                    visibility: visible;
+                }
+                .#{$module}-picture-file-card-replace {
+                    visibility: visible;
+                }
+
+                .#{$module}-picture-file-card-retry {
+                    visibility: visible;
+                }
+                .#{$module}-picture-file-card-preview {
+                    visibility: visible;
+                }
+            }
+
+            &-uploading {
+                &::before {
+                    visibility: visible;
+                }
+            }
+
+            .#{$prefix}-progress-circle {
+                position: absolute;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+            }
         }
     }
 
@@ -542,4 +567,4 @@ $module: #{$prefix}-upload;
     }
 }
 
-@import "./rtl.scss";
+@import './rtl.scss';

+ 3 - 1
packages/semi-foundation/upload/variables.scss

@@ -16,7 +16,7 @@ $color-upload_drag_area_tips-text: var(--semi-color-primary); // 上传可拖拽
 $color-upload_file_card_fail_info-text: var(--semi-color-danger); // 上传文件卡片失败提示信息文本颜色
 $color-upload_file_card_preview_placeholder-bg: rgba(var(--semi-grey-3), 1); // 文件卡片默认预览背景颜色
 $color-upload_file_card_preview_placeholder-text: rgba(var(--semi-white), 1); // 文件卡片默认预览图颜色
-$color-upload_file_card_retry-bg: #fff; // 重新上传按钮背景颜色
+$color-upload_file_card_retry-bg: var(--semi-color-white); // 重新上传按钮背景颜色
 $color-upload_file_card_retry-text: var(--semi-color-primary); // 重新上传按钮文本颜色
 $color-upload-icon: var(--semi-color-tertiary); // 图片墙上传图标加号颜色
 $color-upload_pic_add-bg-active: var(--semi-color-fill-2); // 图片墙上传背景色 - 按下
@@ -26,6 +26,8 @@ $color-upload_pic_remove-bg: var(--semi-color-overlay-bg); // 图片墙上传移
 $color-upload_picture_file_card_loading_error-icon: var(--semi-color-danger); // 图片墙上传移除图标颜色
 $color-upload_picture_file_card_error-border: var(--semi-color-danger); // 图片墙上传移除图标颜色
 $color-upload_picture_file_card_pic_info-text: var(--semi-color-white); // 图片墙图片信息(序号)文字颜色
+$color-upload_picture_file_card_close-icon: var(--semi-color-white); // 图片墙关闭图标颜色
+$color-upload_picture_file_card_hover-bg: var(--semi-color-overlay-bg); // 图片墙预览悬浮背景色
 $color-upload_preview-icon: var(--semi-color-text-2); // 上传文件卡片文本颜色
 $color-upload_retry-text: var(--semi-color-primary); // 上传文件卡片重新上传按钮文本颜色
 $color-upload_replace-text: var(--semi-color-white); // 上传文件卡片重新替换按钮文本颜色

+ 4 - 0
packages/semi-ui/table/Table.tsx

@@ -943,11 +943,15 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
                 titleArr.push(sorter);
             }
 
+            const stateFilteredValue = get(curQuery, 'filteredValue');
+            const defaultFilteredValue = get(curQuery, 'defaultFilteredValue');
+            const filteredValue = stateFilteredValue ? stateFilteredValue : defaultFilteredValue;
             if ((Array.isArray(column.filters) && column.filters.length) || isValidElement(column.filterDropdown)) {
                 const filter = (
                     <ColumnFilter
                         key={strings.DEFAULT_KEY_COLUMN_FILTER}
                         {...curQuery}
+                        filteredValue={filteredValue}
                         onFilterDropdownVisibleChange={(visible: boolean) => this.foundation.toggleShowFilter(dataIndex, visible)}
                         onSelect={(data: OnSelectData) => this.foundation.handleFilterSelect(dataIndex, data)}
                     />

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

@@ -76,8 +76,7 @@ export { default as ScrollBar } from './ScrollBar';
 export { default as TableSpan } from './TableSpan';
 export { default as FixRenderReturnProps } from './FixRenderReturnProps';
 export { default as WarnColumnWithoutDataIndex } from './WarnColumnWithoutDataIndex';
-export { default as FixedColumnsChange } from './v2/FixedColumnsChange';
-export { default as FixedZIndex } from './v2/FixedZIndex';
+export * from './v2';
 
 // empty table
 

+ 123 - 0
packages/semi-ui/table/_story/v2/defaultFilteredValue.tsx

@@ -0,0 +1,123 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { Table, Avatar, Button } from '@douyinfe/semi-ui';
+import * as dateFns from 'date-fns';
+import { ColumnProps, ChangeInfoFilter } from '@douyinfe/semi-ui/table';
+
+const DAY = 24 * 60 * 60 * 1000;
+const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+function App() {
+    const [dataSource, setData] = useState([]);
+    const [filteredValue, setFilteredValue] = useState(['Semi Pro 设计稿']);
+
+    const scroll = useMemo(() => ({ y: 300 }), []);
+
+    const columns: ColumnProps[] = [
+        {
+            title: '标题',
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            filters: [
+                {
+                    text: 'Semi Design 设计稿',
+                    value: 'Semi Design 设计稿',
+                },
+                {
+                    text: 'Semi Pro 设计稿',
+                    value: 'Semi Pro 设计稿',
+                },
+            ],
+            onFilter: (value, record) => record.name.includes(value),
+            sorter: (a, b) => a.name.length - b.name.length > 0 ? 1 : -1,
+            // filterMultiple: false,
+            // filteredValue: filteredValue,
+            defaultFilteredValue: filteredValue,
+        },
+        {
+            title: '大小',
+            dataIndex: 'size',
+            sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
+            render: (text) => `${text} KB`
+        },
+        {
+            title: '所有者',
+            dataIndex: 'owner',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
+                        {text}
+                    </div>
+                );
+            }
+    
+        },
+        {
+            title: '更新日期',
+            dataIndex: 'updateTime',
+            sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
+            render: (value) => {
+                return dateFns.format(new Date(value), 'yyyy-MM-dd');
+            }
+        }
+    ];
+
+    const getData = (total) => {
+        const data = [];
+        for (let i = 0; i < total; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red'
+            });
+        }
+        return data;
+    };
+
+    const handleFilterChange = (filters: ChangeInfoFilter<any>[]) => {
+        console.log('filters', filters);
+        if (Array.isArray(filters) && filters.length) {
+            const { filteredValue } = filters.find(filter => filter.dataIndex === 'name');
+            setFilteredValue(filteredValue);
+        }
+    };
+
+    const handleChange = (options) => {
+        const { filters } = options;
+        handleFilterChange(filters);
+    };
+
+    const toggleChangeData = () => {
+        const length = dataSource.length;
+        const newData = getData(length === 46 ? 25 : 46);
+        setData(newData);
+    };
+
+    useEffect(() => {
+        const data = getData(46);
+        setData(data);
+    }, []);
+
+    return (
+        <div>
+            <Button onClick={toggleChangeData}>toggle change dataSource (46/25)</Button>
+            <Table columns={columns} dataSource={dataSource} scroll={scroll} onChange={handleChange} />
+        </div>
+    );
+}
+
+App.storyName = 'defaultFilteredValue';
+export default App;

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

@@ -0,0 +1,3 @@
+export { default as DefaultFilteredValue } from './defaultFilteredValue';
+export { default as FixedColumnsChange } from './FixedColumnsChange';
+export { default as FixedZIndex } from './FixedZIndex';

+ 1 - 0
packages/semi-ui/table/interface.ts

@@ -79,6 +79,7 @@ export interface ColumnProps<RecordType extends Record<string, any> = any> {
     className?: string;
     colSpan?: number;
     dataIndex?: string;
+    defaultFilteredValue?: any[];
     defaultSortOrder?: SortOrder;
     filterChildrenRecord?: boolean;
     filterDropdown?: React.ReactNode;

+ 83 - 1
packages/semi-ui/tooltip/_story/tooltip.stories.js

@@ -1,7 +1,7 @@
 import React, { useState, useMemo } from 'react';
 import Tooltip from '../index';
 import './story.scss';
-import { Tag, Icon, IconButton, Switch, Checkbox, Radio, Button, Select, InputNumber } from '@douyinfe/semi-ui';
+import { Tag, Icon, IconButton, Switch, Checkbox, Radio, Button, Select, InputNumber, Space } from '@douyinfe/semi-ui';
 
 import InTableDemo from './InTable';
 import EdgeDemo from './Edge';
@@ -736,3 +736,85 @@ export const AutoAdjustWithSpacing = () => {
 AutoAdjustWithSpacing.story = {
   name: 'AutoAdjustWithSpacing',
 };
+
+/**
+ * Chromatic UI test
+ */
+export const leftTopOverDemo = () => {
+    const [visible, setVisible] = useState(true);
+    const content = (
+        <div style={{ height: 200, width: 200 }}>
+            Semi Design
+        </div>
+    );
+
+    const commonProps = {
+        content,
+        trigger: 'click',
+        showArrow: false,
+        visible,
+        trigger: 'custom',
+        motion: false,
+    };
+    const buttonStyle = {
+        width: 200,
+    };
+
+    return (
+        <div data-cy='wrapper'>
+            <Button onClick={() => setVisible(!visible)}>toggle visible</Button>
+            <div style={{ paddingTop: 200 }}>
+                <Space spacing={80}>
+                    <Tooltip {...commonProps} position='leftTopOver' >
+                        <Button data-cy='leftTopOver' style={buttonStyle}>leftTopOver</Button>
+                    </Tooltip>
+                    <Tooltip {...commonProps} position='leftBottomOver'>
+                        <Button data-cy='leftBottomOver' style={buttonStyle}>leftBottomOver</Button>
+                    </Tooltip>
+                    <Tooltip {...commonProps} position='rightTopOver'>
+                        <Button data-cy='rightTopOver' style={buttonStyle}>rightTopOver</Button>
+                    </Tooltip>
+                    <Tooltip {...commonProps} position='rightBottomOver'>
+                        <Button data-cy='rightBottomOver' style={buttonStyle}>rightBottomOver</Button>
+                    </Tooltip>
+                </Space>
+            </div>
+        </div>
+    )
+};
+leftTopOverDemo.storyName = `leftTopOver visible`;
+leftTopOverDemo.parameters = { 
+    chromatic: {
+        disableSnapshot: false,
+        delay: 3000,
+        viewports: [1200]
+    }
+};
+
+/**
+ * Cypress test
+ */
+export const leftTopOverAutoAdjustOverflow = () => {
+    const content = (
+        <div style={{ height: 200, width: 200 }}>
+            Semi Design
+        </div>
+    );
+
+    const commonProps = {
+        content,
+        trigger: 'click',
+        showArrow: false,
+    };
+
+    return (
+        <div data-cy='wrapper' style={{ width: '200vw', height: '200vw', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
+            <div>
+                <Tooltip {...commonProps} position='leftTopOver' >
+                    <Button data-cy='leftTopOver' style={{ width: 200 }}>leftTopOver</Button>
+                </Tooltip>
+            </div>
+        </div>
+    )
+};
+leftTopOverAutoAdjustOverflow.storyName = `leftTopOver autoAdjustOverflow`;

+ 22 - 6
packages/semi-ui/upload/_story/upload.stories.js

@@ -2,7 +2,7 @@
 import React, { useState } from 'react';
 import { Upload, Button, Toast, Icon } from '@douyinfe/semi-ui/index';
 import { withField, Form } from '../../form/index';
-import { IconPlus, IconFile, IconUpload } from '@douyinfe/semi-icons';
+import { IconPlus, IconFile, IconUpload, IconEyeOpened, IconDownload, IconDelete } from '@douyinfe/semi-icons';
 
 import FileCard from '../fileCard';
 
@@ -50,7 +50,7 @@ let commonProps = {
     let url = fileItem.url;
     console.log(fileItem);
     window.open(url);
-  },
+  }
 };
 
 export const BasicUsage = () => (
@@ -270,7 +270,7 @@ const defaultFileList = [
       'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg',
   },
   {
-    uid: '5',
+    uid: '3',
     name: 'jiafang3.jpeg',
     status: 'uploading',
     percent: 50,
@@ -279,7 +279,7 @@ const defaultFileList = [
       'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg',
   },
   {
-    uid: '5',
+    uid: '4',
     name: 'jiafang3.jpeg',
     status: 'validateFail',
     validateMessage: '文件过大',
@@ -288,7 +288,7 @@ const defaultFileList = [
       'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg',
   },
   {
-    uid: '4',
+    uid: '5',
     name: 'jiafang4.jpeg',
     status: 'validating',
     validateMessage: '校验中',
@@ -399,11 +399,12 @@ PictureListType.story = {
 export const PictureListTypeWithDefaultFileList = () => (
   <>
     <Upload
-      showReplace
       {...commonProps}
+      showReplace={false}
       action={action}
       listType="picture"
       accept="image/*"
+      renderPicPreviewIcon={()=><IconEyeOpened style={{color: 'var(--semi-color-white)',fontSize: 24}} />}
       defaultFileList={defaultFileList}
     >
       <React.Fragment>
@@ -941,3 +942,18 @@ export const _ForbiddenRemove = () => <ForbiddenRemove />;
 _ForbiddenRemove.story = {
   name: 'forbidden remove',
 };
+
+export const CustomListOperation = () => {
+  const renderFileOperation = (fileItem)=>{
+    return <div style={{display: 'flex',columnGap: 8, padding: '0 8px'}}>
+      <Button icon={<IconEyeOpened></IconEyeOpened>} type="tertiary" theme="borderless" size="small"></Button>
+      <Button icon={<IconDownload></IconDownload>} type="tertiary" theme="borderless" size="small"></Button>
+      <Button onClick={e=>fileItem.onRemove()} icon={<IconDelete></IconDelete>} type="tertiary" theme="borderless" size="small"></Button>
+    </div>
+  }
+  return <Upload defaultFileList={defaultFileList} itemStyle={{width: 300}} renderFileOperation={renderFileOperation}><Button icon={<IconUpload />} theme="light">点击上传</Button></Upload>
+}
+
+CustomListOperation.story = {
+  name: 'custom list operation',
+}

+ 23 - 23
packages/semi-ui/upload/fileCard.tsx

@@ -3,11 +3,11 @@ import cls from 'classnames';
 import PropTypes from 'prop-types';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/upload/constants';
 import { getFileSize } from '@douyinfe/semi-foundation/upload/utils';
-import { IconAlertCircle, IconClose, IconFile, IconRefresh } from '@douyinfe/semi-icons';
+import { IconAlertCircle, IconClose, IconClear, IconFile, IconRefresh, IconEyeOpened } from '@douyinfe/semi-icons';
 import LocaleConsumer from '../locale/localeConsumer';
 import { Locale } from '../locale/interface';
 
-import IconButton from '../iconButton/index';
+import Button from '../button/index';
 import Progress from '../progress/index';
 import Tooltip from '../tooltip/index';
 import Spin from '../spin/index';
@@ -123,10 +123,11 @@ class FileCard extends PureComponent<FileCardProps> {
     }
 
     renderPic(locale: Locale['Upload']): ReactNode {
-        const { url, percent, status, disabled, style, onPreviewClick, showPicInfo, renderPicInfo, renderThumbnail, name, index } = this.props;
+        const { url, percent, status, disabled, style, onPreviewClick, showPicInfo, renderPicInfo, renderPicPreviewIcon, renderThumbnail, name, index } = this.props;
         const showProgress = status === strings.FILE_STATUS_UPLOADING && percent !== 100;
         const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && this.props.showRetry;
         const showReplace = status === strings.FILE_STATUS_SUCCESS && this.props.showReplace;
+        const showPreview = status === strings.FILE_STATUS_SUCCESS && !this.props.showReplace;
         const filePicCardCls = cls({
             [`${prefixCls}-picture-file-card`]: true,
             [`${prefixCls}-picture-file-card-disabled`]: disabled,
@@ -134,7 +135,6 @@ class FileCard extends PureComponent<FileCardProps> {
             [`${prefixCls}-picture-file-card-error`]: status === strings.FILE_STATUS_UPLOAD_FAIL,
             [`${prefixCls}-picture-file-card-uploading`]: showProgress
         });
-        const closeCls = `${prefixCls}-picture-file-card-close`;
         const retry = (
             <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
                 <IconRefresh className={`${prefixCls}-picture-file-card-icon-retry`} />
@@ -146,7 +146,16 @@ class FileCard extends PureComponent<FileCardProps> {
                     <ReplaceSvg className={`${prefixCls}-picture-file-card-icon-replace`} />
                 </div>
             </Tooltip>
-
+        );
+        const preview = (
+            <div className={`${prefixCls}-picture-file-card-preview`}>
+                {typeof renderPicPreviewIcon === 'function'? renderPicPreviewIcon(this.props): null}
+            </div>
+        );
+        const close = (
+            <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-close`} onClick={e => this.onRemove(e)}>
+                <IconClear className={`${prefixCls}-picture-file-card-icon-close`} />
+            </div>
         );
 
         const picInfo = typeof renderPicInfo === 'function' ? renderPicInfo(this.props) : (
@@ -161,19 +170,16 @@ class FileCard extends PureComponent<FileCardProps> {
                 {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} aria-label="uploading file progress" /> : null}
                 {showRetry ? retry : null}
                 {showReplace && replace}
+                {showPreview && preview}
                 {showPicInfo && picInfo}
-                {!disabled && (
-                    <div className={closeCls} onClick={e => this.onRemove(e)}>
-                        <IconClose tabIndex={0} role="button" size="extra-small" />
-                    </div>
-                )}
+                {!disabled && close}
                 {this.renderPicValidateMsg()}
             </div>
         );
     }
 
     renderFile(locale: Locale["Upload"]) {
-        const { name, size, percent, url, showRetry: propsShowRetry, showReplace: propsShowReplace, preview, previewFile, status, style, onPreviewClick } = this.props;
+        const { name, size, percent, url, showRetry: propsShowRetry, showReplace: propsShowReplace, preview, previewFile, status, style, onPreviewClick, renderFileOperation } = this.props;
         const fileCardCls = cls({
             [`${prefixCls}-file-card`]: true,
             [`${prefixCls}-file-card-fail`]: status === strings.FILE_STATUS_VALID_FAIL || status === strings.FILE_STATUS_UPLOAD_FAIL,
@@ -195,6 +201,7 @@ class FileCard extends PureComponent<FileCardProps> {
         if (previewFile) {
             previewContent = previewFile(this.props);
         }
+        const operation = typeof renderFileOperation === 'function'? renderFileOperation(this.props) : <Button onClick={e => this.onRemove(e)} type="tertiary" icon={<IconClose />} theme="borderless" size="small" className={closeCls} />;
         return (
             <div role="listitem" className={fileCardCls} style={style} onClick={onPreviewClick}>
                 <div className={previewCls}>
@@ -209,7 +216,7 @@ class FileCard extends PureComponent<FileCardProps> {
                             <span className={`${infoCls}-size`}>{fileSize}</span>
                             {showReplace && (
                                 <Tooltip trigger="hover" position="top" showArrow={false} content={locale.replace}>
-                                    <IconButton
+                                    <Button
                                         onClick={e => this.onReplace(e)}
                                         type="tertiary"
                                         theme="borderless"
@@ -231,31 +238,24 @@ class FileCard extends PureComponent<FileCardProps> {
                         {showRetry ? <span role="button" tabIndex={0} className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
                     </div>
                 </div>
-                <IconButton
-                    onClick={e => this.onRemove(e)}
-                    type="tertiary"
-                    icon={<IconClose />}
-                    theme="borderless"
-                    size="small"
-                    className={closeCls}
-                />
+                {operation}
             </div>
         );
     }
 
     onRemove(e: MouseEvent): void {
         e.stopPropagation();
-        this.props.onRemove(this.props, e);
+        this.props.onRemove();
     }
 
     onReplace(e: MouseEvent): void {
         e.stopPropagation();
-        this.props.onReplace(this.props, e);
+        this.props.onReplace();
     }
 
     onRetry(e: MouseEvent): void {
         e.stopPropagation();
-        this.props.onRetry(this.props, e);
+        this.props.onRetry();
     }
 
     render() {

+ 15 - 6
packages/semi-ui/upload/index.tsx

@@ -2,7 +2,7 @@
 import React, { ReactNode, CSSProperties, RefObject, ChangeEvent, DragEvent } from 'react';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
-import { noop } from 'lodash';
+import { noop, pick } from 'lodash';
 import UploadFoundation, { CustomFile, UploadAdapter, BeforeUploadObjectResult, AfterUploadResult } from '@douyinfe/semi-foundation/upload/foundation';
 import { strings, cssClasses } from '@douyinfe/semi-foundation/upload/constants';
 import FileCard from './fileCard';
@@ -39,6 +39,7 @@ export interface UploadProps {
     fileList?: Array<FileItem>;
     fileName?: string;
     headers?: Record<string, any> | ((file: File) => Record<string, string>);
+    hotSpotLocation?: 'start' | 'end';
     itemStyle?: CSSProperties;
     limit?: number;
     listType?: UploadListType;
@@ -66,6 +67,8 @@ export interface UploadProps {
     renderFileItem?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
     renderPicInfo?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
     renderThumbnail?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
+    renderPicPreviewIcon?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
+    renderFileOperation?: (fileItem: RenderFileItemProps) => ReactNode;
     showClear?: boolean;
     showPicInfo?: boolean; // Show pic info in picture wall
     showReplace?: boolean; // Display replacement function
@@ -111,6 +114,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         fileList: PropTypes.array, // files had been uploaded
         fileName: PropTypes.string, // same as name, to avoid props conflict in Form.Upload
         headers: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+        hotSpotLocation: PropTypes.oneOf(['start','end']),
         itemStyle: PropTypes.object,
         limit: PropTypes.number, // 最大允许上传文件个数
         listType: PropTypes.oneOf<UploadProps['listType']>(strings.LIST_TYPE),
@@ -136,6 +140,8 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         prompt: PropTypes.node,
         promptPosition: PropTypes.oneOf<UploadProps['promptPosition']>(strings.PROMPT_POSITION),
         renderFileItem: PropTypes.func,
+        renderPicPreviewIcon: PropTypes.func,
+        renderFileOperation: PropTypes.func,
         renderPicInfo: PropTypes.func,
         renderThumbnail: PropTypes.func,
         showClear: PropTypes.bool,
@@ -156,6 +162,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         defaultFileList: [],
         disabled: false,
         listType: 'list' as const,
+        hotSpotLocation: 'end',
         multiple: false,
         onAcceptInvalid: noop,
         onChange: noop,
@@ -326,7 +333,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
 
     renderFile = (file: FileItem, index: number, locale: Locale['Upload']): ReactNode => {
         const { name, status, validateMessage, _sizeInvalid, uid } = file;
-        const { previewFile, listType, itemStyle, showRetry, showPicInfo, renderPicInfo, renderFileItem, renderThumbnail, disabled, onPreviewClick, showReplace } = this.props;
+        const { previewFile, listType, itemStyle, showPicInfo, renderPicInfo, renderPicPreviewIcon, renderFileOperation, renderFileItem, renderThumbnail, disabled, onPreviewClick } = this.props;
         const onRemove = (): void => this.remove(file);
         const onRetry = (): void => {
             this.foundation.retry(file);
@@ -335,6 +342,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             this.replace(index);
         };
         const fileCardProps = {
+            ...pick(this.props, ['showRetry', 'showReplace', '']),
             ...file,
             previewFile,
             listType,
@@ -342,13 +350,13 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             onRetry,
             index,
             key: uid || `${name}${index}`,
-            showRetry: typeof file.showRetry !== 'undefined' ? file.showRetry : showRetry,
             style: itemStyle,
             disabled,
             showPicInfo,
             renderPicInfo,
+            renderPicPreviewIcon,
+            renderFileOperation,
             renderThumbnail,
-            showReplace: typeof file.showReplace !== 'undefined' ? file.showReplace : showReplace,
             onReplace,
             onPreviewClick: typeof onPreviewClick !== 'undefined' ? (): void => this.foundation.handlePreviewClick(file) : undefined,
         };
@@ -382,7 +390,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
     };
 
     renderFileListPic = () => {
-        const { showUploadList, limit, disabled, children, draggable } = this.props;
+        const { showUploadList, limit, disabled, children, draggable, hotSpotLocation } = this.props;
         const { fileList: stateFileList, dragAreaStatus } = this.state;
         const fileList = this.props.fileList || stateFileList;
         const showAddTriggerInList = limit ? limit > fileList.length : true;
@@ -434,8 +442,9 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
                 {(locale: Locale['Upload']) => (
                     <div {...containerProps}>
                         <div className={mainCls} role="list" aria-label="picture list">
+                            {showAddTriggerInList && hotSpotLocation === 'start' ? addContent : null}
                             {fileList.map((file, index) => this.renderFile(file, index, locale))}
-                            {showAddTriggerInList ? addContent : null}
+                            {showAddTriggerInList && hotSpotLocation === 'end' ? addContent : null}
                         </div>
                     </div>
                 )}

+ 7 - 5
packages/semi-ui/upload/interface.ts

@@ -48,14 +48,16 @@ export interface RenderFileItemProps extends FileItem {
     index?: number;
     previewFile?: (fileItem: RenderFileItemProps) => ReactNode;
     listType: UploadListType;
-    onRemove: (props: RenderFileItemProps, e: MouseEvent) => void;
-    onRetry: (props: RenderFileItemProps, e: MouseEvent) => void;
-    onReplace: (props: RenderFileItemProps, e: MouseEvent) => void;
+    onRemove: () => void;
+    onRetry: () => void;
+    onReplace: () => void;
     key: string;
     showPicInfo?: boolean;
     renderPicInfo?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
-    showRetry: boolean;
-    showReplace: boolean;
+    renderPicPreviewIcon?: (renderFileItemProps: RenderFileItemProps) => ReactNode;
+    renderFileOperation?: (fileItem: RenderFileItemProps) => ReactNode;
+    showRetry?: boolean;
+    showReplace?: boolean;
     style?: CSSProperties;
     disabled: boolean;
     onPreviewClick: () => void;