Browse Source

feat: Table support renderFilterDropdown #2015 (#2046)

* feat: Table support renderFilterDropdown #2015

* feat: Table renderFilterDropdown update props name

* docs: Table add title filter demo #2015

---------

Co-authored-by: shijia.me <[email protected]>
Co-authored-by: pointhalo <[email protected]>
Shi Jia 1 year ago
parent
commit
d9a51738a7

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

@@ -1318,6 +1318,296 @@ function App() {
 render(App);
 ```
 
+### Custom Header Filtering
+
+If you need to display the filter input box in the table header, you can pass ReactNode in the `title` and use it with `filteredValue`.
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Space } from '@douyinfe/semi-ui';
+import * as dateFns from 'date-fns';
+
+function App() {
+    const [dataSource, setData] = useState([]);
+    const [filteredValue, setFilteredValue] = useState([]);
+    const compositionRef = useRef({ isComposition: false });
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+
+    const handleChange = (value) => {
+        if (compositionRef.current.isComposition) {
+            return;
+        }
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+    const handleCompositionStart = () => {
+        compositionRef.current.isComposition = true;
+    };
+
+    const handleCompositionEnd = (event) => {
+        compositionRef.current.isComposition = false;
+        const value = event.target.value;
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+
+
+    const columns = [
+        {
+            title: (
+                <Space>
+                    <span>Title</span>
+                    <Input
+                        placeholder="Input filter value"
+                        style={{ width: 200 }}
+                        onCompositionStart={handleCompositionStart}
+                        onCompositionEnd={handleCompositionEnd}
+                        onChange={handleChange}
+                        showClear 
+                    />
+                </Space>
+            ),
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            filteredValue,
+        },
+        {
+            title: 'Size',
+            dataIndex: 'size',
+            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
+            render: text => `${text} KB`,
+        },
+        {
+            title: 'Owner',
+            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: 'Update',
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi D2C design draft${i}.fig`,
+                owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}
+
+render(App);
+```
+
+
+
+### Custom Filter Rendering
+
+Use `renderFilterDropdown` to customize the render filter panel. v2.52 supported.
+
+You can call `setTempFilteredValue` to store the filter value when the user enters the filter value, and call `confirm` to trigger the actual filtering after the filter value is entered. You can also filter directly through `confirm({ filteredValue })`.
+
+The reason for setting `tempFilteredValue` is that in scenarios where temporary filtered values need to be stored, there is no need to declare a state to save this temporary filtered value.
+
+```typescript
+type RenderFilterDropdown = (props?: RenderFilterDropdownProps) => React.ReactNode;
+interface RenderFilterDropdownProps {
+     /** Temporary filter value, the initial value is `filteredValue` or `defaultFilteredValue` */
+     tempFilteredValue: any[];
+     /** Set temporary filter value */
+     setTempFilteredValue: (tempFilteredValue: any[]) => void;
+     /** `confirm` will assign `tempFilteredValue` to `filteredValue` by default and trigger the `onChange` event. You can also set the filter value directly by passing in `filteredValue` */
+     confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
+     /** Clear filter values and temporary filter values */
+     clear: (props?: { closeDropdown?: boolean }) => void;
+     /** Close dropdown */
+     close: () => void;
+     /** Filter configuration items, do not pass if not required */
+     filters?: RenderDropdownProps['filters']
+}
+```
+
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Button, Space } from '@douyinfe/semi-ui';
+import * as dateFns from 'date-fns';
+
+function App() {
+    const [dataSource, setData] = useState([]);
+    const inputRef = useRef();
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+    const columns = [
+        {
+            title: 'Title',
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = value => {
+                    const filteredValue = value ? [value] : [];
+                    setTempFilteredValue(filteredValue);
+                    // You can also filter directly when the input value changes
+                    // confirm({ filteredValue });
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input ref={inputRef} value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: true })}>Filter+Close</Button>
+                            <Button onClick={() => clear({ closeDropdown: true })}>Clear+Close</Button>
+                            <Button onClick={() => close()}>Close</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+            onFilterDropdownVisibleChange: (visible) => {
+                console.log('inputRef', visible, inputRef);
+                if (inputRef.current && inputRef.current.focus) {
+                    inputRef.current.focus();
+                }
+            }
+        },
+        {
+            title: 'Size',
+            dataIndex: 'size',
+            sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
+            render: text => `${text} KB`,
+        },
+        {
+            title: 'Owner',
+            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>
+                );
+            },
+            onFilter: (value, record) => record.owner.includes(value),
+            defaultFilteredValue: ['Jiang Pengzhi'],
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = (value) => {
+                    if (value) {
+                        setTempFilteredValue([value]);
+                    } else {
+                        setTempFilteredValue([]);
+                    }
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: false })}>Filter+Close</Button>
+                            <Button onClick={() => clear({ closeDropdown: false })}>Clear+Close</Button>
+                            <Button onClick={() => close()}>Close</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+        },
+        {
+            title: 'Update',
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi D2C design draft${i}.fig`,
+                owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}
+
+render(App);
+```
+
 ### Custom Filter Item Rendering
 
 Since the **1.1.0** version, it is supported to pass in `renderFilterDropdownItem` to customize the rendering method of each filter item.
@@ -5142,6 +5432,7 @@ import { Table } from '@douyinfe/semi-ui';
 | fixed | Whether the column is fixed, optional true (equivalent to left) 'left' 'right' | boolean\|string | false |
 | key | The key required by React, if a unique dataIndex has been set, can ignore this property | string |  |
 | render | A rendering function that generates complex data, the parameters are the value of the current row, the current row data, the row index, and the table row / column merge can be set in return object | (text: any, record: RecordType, index: number, { expandIcon?: ReactNode, selection?: ReactNode, indentText?: ReactNode }) => React\|object |  |
+| renderFilterDropdown | Custom filter dropdown panel, for usage details, see [Custom Filter Rendering](#Custom-Filter-Rendering) | (props?: RenderFilterDropdownProps) => React.ReactNode; | - | **2.52.0** |
 | renderFilterDropdownItem | Customize the rendering method of each filter item. For usage details, see [Custom Filter Item Rendering](#Custom-Filter-Item-Rendering) | ({ value: any, text: any, onChange: Function, level: number, ...otherProps }) => ReactNode | - | **1.1.0** |
 | resize | Whether to enable resize mode, this property will take effect only after Table resizable is enabled | boolean |  | **2.42.0** |
 | sortChildrenRecord | Whether to sort child data locally | boolean |  | **0.29.0** |

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

@@ -1324,6 +1324,294 @@ function App() {
 render(App);
 ```
 
+### 自定义表头筛选
+
+如果你需要将筛选器输入框展示在表头,可在 `title` 传入 ReactNode,配合 `filteredValue` 使用。
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Space } from '@douyinfe/semi-ui';
+import * as dateFns from 'date-fns';
+
+function App() {
+    const [dataSource, setData] = useState([]);
+    const [filteredValue, setFilteredValue] = useState([]);
+    const compositionRef = useRef({ isComposition: false });
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+
+    const handleChange = (value) => {
+        if (compositionRef.current.isComposition) {
+            return;
+        }
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+    const handleCompositionStart = () => {
+        compositionRef.current.isComposition = true;
+    };
+
+    const handleCompositionEnd = (event) => {
+        compositionRef.current.isComposition = false;
+        const value = event.target.value;
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+
+
+    const columns = [
+        {
+            title: (
+                <Space>
+                    <span>标题</span>
+                    <Input
+                        placeholder="请输入筛选值"
+                        style={{ width: 200 }}
+                        onCompositionStart={handleCompositionStart}
+                        onCompositionEnd={handleCompositionEnd}
+                        onChange={handleChange}
+                        showClear 
+                    />
+                </Space>
+            ),
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}
+
+render(App);
+```
+
+### 自定义筛选器
+
+使用 `renderFilterDropdown` 自定义渲染筛选器面板。v2.52 支持。
+
+你可以在用户输入筛选值的时候调用 `setTempFilteredValue` 存储筛选值,在筛选值输入完毕后调用 `confirm` 触发真正的筛选。也可以通过 `confirm({ filteredValue })` 直接筛选。
+
+设置 `tempFilteredValue` 的原因是在需要存储临时筛选值的场景,不需要自己声明一个 state 保存这个临时筛选值。
+
+```typescript
+type RenderFilterDropdown = (props?: RenderFilterDropdownProps) => React.ReactNode;
+interface RenderFilterDropdownProps {
+    /** 临时筛选值,初始值为 `filteredValue` 或 `defaultFilteredValue`  */
+    tempFilteredValue: any[];
+    /** 设置临时筛选值  */
+    setTempFilteredValue: (tempFilteredValue: any[]) => void;
+    /** `confirm` 默认会将 `tempFilteredValue` 赋值给 `filteredValue` 并触发 `onChange` 事件。你也可以通过传入 `filteredValue` 直接设置筛选值  */
+    confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
+    /** 清除筛选值、临时筛选值  */
+    clear: (props?: { closeDropdown?: boolean }) => void;
+    /** 关闭 dropdown  */
+    close: () => void;
+    /** 筛选器配置项,如不需要可以不传  */
+    filters?: RenderDropdownProps['filters']
+}
+```
+
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Button, Space } from '@douyinfe/semi-ui';
+import * as dateFns from 'date-fns';
+
+function App() {
+    const [dataSource, setData] = useState([]);
+    const inputRef = useRef();
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+    const columns = [
+        {
+            title: '标题',
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = value => {
+                    const filteredValue = value ? [value] : [];
+                    setTempFilteredValue(filteredValue);
+                    // 你也可以在 input value 变化时直接筛选
+                    // confirm({ filteredValue });
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input ref={inputRef} value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: true })}>筛选+关闭</Button>
+                            <Button onClick={() => clear({ closeDropdown: true })}>清除+关闭</Button>
+                            <Button onClick={() => close()}>直接关闭</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+            onFilterDropdownVisibleChange: (visible) => {
+                console.log('inputRef', visible, inputRef);
+                if (inputRef.current && inputRef.current.focus) {
+                    inputRef.current.focus();
+                }
+            }
+        },
+        {
+            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>
+                );
+            },
+            onFilter: (value, record) => record.owner.includes(value),
+            defaultFilteredValue: ['姜鹏志'],
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = (value) => {
+                    if (value) {
+                        setTempFilteredValue([value]);
+                    } else {
+                        setTempFilteredValue([]);
+                    }
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: false })}>筛选后不关闭</Button>
+                            <Button onClick={() => clear({ closeDropdown: false })}>清除后不关闭</Button>
+                            <Button onClick={() => close()}>直接关闭</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+        },
+        {
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}
+
+render(App);
+```
+
 
 
 ### 自定义筛选项渲染
@@ -5158,6 +5446,7 @@ import { Table } from '@douyinfe/semi-ui';
 | fixed | 列是否固定,可选 true(等效于 left) 'left' 'right',在 RTL 时会自动切换 | boolean\|string | false |
 | key | React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 | string |  |
 | render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并 | (text: any, record: RecordType, index: number, { expandIcon?: ReactNode, selection?: ReactNode, indentText?: ReactNode }) => object\|ReactNode |  |
+| renderFilterDropdown | 自定义筛选器 dropdown 面板,用法详见[自定义筛选器](#自定义筛选器) | (props?: RenderFilterDropdownProps) => React.ReactNode; | - | **2.52.0** |
 | renderFilterDropdownItem | 自定义每个筛选项渲染方式,用法详见[自定义筛选项渲染](#自定义筛选项渲染) | ({ value: any, text: any, onChange: Function, level: number, ...otherProps }) => ReactNode | - | **1.1.0** |
 | resize | 是否开启 resize 模式,只有 Table resizable 开启后此属性才会生效 | boolean |  | **2.42.0** |
 | sortChildrenRecord | 是否对子级数据进行本地排序 | boolean |  | **0.29.0** |

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

@@ -246,4 +246,37 @@ describe('table', () => {
         cy.get('button').contains('点击全选').click();
         cy.get('.semi-table-thead .semi-checkbox-checked').should('exist')
     });
+
+    it('test renderFilterDropdown', () => {
+        cy.visit('http://localhost:6006/iframe.html?args=&id=table--feat-render-filter-dropdown&viewMode=story');
+
+        // 测试第一个筛选器
+        cy.get('.semi-table-column-filter').eq(0).click();
+        cy.get('.semi-input').should('be.focused');
+        cy.get('.semi-input').type('12');
+        cy.get('.semi-button').contains('筛选+关闭').click();
+        cy.get('.semi-table-tbody .semi-table-row').should('have.length', 1);
+        cy.get('.semi-table-column-filter').eq(0).click();
+        cy.get('.semi-input').should('be.focused');
+        cy.get('.semi-button').contains('清除+关闭').click();
+        cy.get('.semi-table-tbody .semi-table-row').should('have.length', 10);
+        cy.get('.semi-table-column-filter').eq(0).click();
+        cy.get('.semi-input').should('be.focused');
+        cy.get('.semi-button').contains('直接关闭').click();
+        cy.get('.semi-dropdown').should('not.exist');
+
+        // 测试第二个筛选器
+        cy.get('.semi-table-column-filter').eq(1).click();
+        cy.get('.semi-input').should('have.value', '姜鹏志');
+        cy.get('.semi-button').contains('清除后不关闭').click();
+        cy.get('.semi-table-pagination-info').should('contain', '显示第 1 条-第 10 条,共 46 条');
+        cy.get('.semi-dropdown').should('exist');
+        cy.get('.semi-table-column-filter').eq(1).click();
+        cy.get('.semi-input').type('郝宣');
+        cy.get('.semi-button').contains('筛选后不关闭').click();
+        cy.get('.semi-table-pagination-info').should('contain', '显示第 1 条-第 10 条,共 23 条');
+        cy.get('.semi-dropdown').should('exist');
+        cy.get('.semi-button').contains('直接关闭').click();
+        cy.get('.semi-dropdown').should('not.exist');
+    });
 });

+ 187 - 109
packages/semi-ui/table/ColumnFilter.tsx

@@ -1,12 +1,11 @@
-import React, { isValidElement } from 'react';
+import React, { isValidElement, useEffect, useState } from 'react';
 import cls from 'classnames';
-import { noop } from 'lodash';
+import { isEqual, noop, pick } from 'lodash';
 import { IconFilter } from '@douyinfe/semi-icons';
 
 import { cssClasses } from '@douyinfe/semi-foundation/table/constants';
 
 import Dropdown, { DropdownProps } from '../dropdown';
-import { Trigger, Position } from '../tooltip';
 import { Radio } from '../radio';
 import { Checkbox } from '../checkbox';
 import {
@@ -16,7 +15,7 @@ import {
     RenderFilterDropdownItem
 } from './interface';
 
-function renderDropdown(props: RenderDropdownProps = {}, nestedElem: React.ReactNode = null, level = 0) {
+function renderDropdown(props: RenderDropdownProps, nestedElem: React.ReactNode = null, level = 0) {
     const {
         filterMultiple = true,
         filters = [],
@@ -26,95 +25,99 @@ function renderDropdown(props: RenderDropdownProps = {}, nestedElem: React.React
         onFilterDropdownVisibleChange = noop,
         trigger = 'click',
         position = 'bottom',
+        renderFilterDropdown,
         renderFilterDropdownItem,
-    } = props;
+    } = props ?? {};
+
+    const renderFilterDropdownProps: RenderFilterDropdownProps = pick(props, ['tempFilteredValue', 'setTempFilteredValue', 'confirm', 'clear', 'close', 'filters']);
+    const render = typeof renderFilterDropdown === 'function' ? renderFilterDropdown(renderFilterDropdownProps) : (
+        <Dropdown.Menu>
+            {Array.isArray(filters) &&
+                filters.map((filter, index) => {
+                    const changeFn = (e: React.MouseEvent<HTMLLIElement>) => {
+                        const domEvent = e && e.nativeEvent;
+                        if (domEvent) {
+                            // Block this event to prevent the pop-up layer from closing
+                            domEvent.stopImmediatePropagation();
+
+                            // Prevent bubbling and default events to prevent label click events from triggering twice
+                            domEvent.stopPropagation();
+                            domEvent.preventDefault();
+                        }
+                        let values = [...filteredValue];
+
+                        const included = values.includes(filter.value);
+                        const idx = values.indexOf(filter.value);
+
+                        if (idx > -1) {
+                            values.splice(idx, 1);
+                        } else if (filterMultiple) {
+                            values.push(filter.value);
+                        } else {
+                            values = [filter.value];
+                        }
+                        return onSelect({
+                            value: filter.value,
+                            filteredValue: values,
+                            included: !included,
+                            domEvent,
+                        });
+                    };
+
+                    const checked = filteredValue.includes(filter.value);
+                    const { text } = filter;
+                    const { value } = filter;
+                    const key = `${level}_${index}`;
+
+                    const dropdownItem =
+                        typeof renderFilterDropdownItem === 'function' ?
+                            renderFilterDropdownItem({
+                                onChange: changeFn,
+                                filterMultiple,
+                                value,
+                                text,
+                                checked,
+                                filteredValue,
+                                level,
+                            }) :
+                            null;
+
+                    let item =
+                        dropdownItem && React.isValidElement(dropdownItem) ? (
+                            React.cloneElement(dropdownItem, { key })
+                        ) : (
+                            <Dropdown.Item key={key} onClick={changeFn}>
+                                {filterMultiple ? (
+                                    <Checkbox checked={checked}>{text}</Checkbox>
+                                ) : (
+                                    <Radio checked={checked}>{text}</Radio>
+                                )}
+                            </Dropdown.Item>
+                        );
+
+                    if (Array.isArray(filter.children) && filter.children.length) {
+                        const childrenDropdownProps = {
+                            ...props,
+                            filters: filter.children,
+                            trigger: 'hover' as const,
+                            position: 'right' as const,
+                        };
+
+                        delete childrenDropdownProps.filterDropdownVisible;
+
+                        item = renderDropdown(childrenDropdownProps, item, level + 1);
+                    }
+                    return item;
+                })}
+        </Dropdown.Menu>
+    );
 
     const dropdownProps: DropdownProps = {
         ...props,
         onVisibleChange: (visible: boolean) => onFilterDropdownVisibleChange(visible),
         trigger,
         position,
-        render: (
-            <Dropdown.Menu>
-                {Array.isArray(filters) &&
-                    filters.map((filter, index) => {
-                        const changeFn = (e: React.MouseEvent<HTMLLIElement>) => {
-                            const domEvent = e && e.nativeEvent;
-                            if (domEvent) {
-                                // Block this event to prevent the pop-up layer from closing
-                                domEvent.stopImmediatePropagation();
-
-                                // Prevent bubbling and default events to prevent label click events from triggering twice
-                                domEvent.stopPropagation();
-                                domEvent.preventDefault();
-                            }
-                            let values = [...filteredValue];
-
-                            const included = values.includes(filter.value);
-                            const idx = values.indexOf(filter.value);
-
-                            if (idx > -1) {
-                                values.splice(idx, 1);
-                            } else if (filterMultiple) {
-                                values.push(filter.value);
-                            } else {
-                                values = [filter.value];
-                            }
-                            return onSelect({
-                                value: filter.value,
-                                filteredValue: values,
-                                included: !included,
-                                domEvent,
-                            });
-                        };
-
-                        const checked = filteredValue.includes(filter.value);
-                        const { text } = filter;
-                        const { value } = filter;
-                        const key = `${level}_${index}`;
-
-                        const dropdownItem =
-                            typeof renderFilterDropdownItem === 'function' ?
-                                renderFilterDropdownItem({
-                                    onChange: changeFn,
-                                    filterMultiple,
-                                    value,
-                                    text,
-                                    checked,
-                                    filteredValue,
-                                    level,
-                                }) :
-                                null;
-
-                        let item =
-                            dropdownItem && React.isValidElement(dropdownItem) ? (
-                                React.cloneElement(dropdownItem, { key })
-                            ) : (
-                                <Dropdown.Item key={key} onClick={changeFn}>
-                                    {filterMultiple ? (
-                                        <Checkbox checked={checked}>{text}</Checkbox>
-                                    ) : (
-                                        <Radio checked={checked}>{text}</Radio>
-                                    )}
-                                </Dropdown.Item>
-                            );
-
-                        if (Array.isArray(filter.children) && filter.children.length) {
-                            const childrenDropdownProps = {
-                                ...props,
-                                filters: filter.children,
-                                trigger: 'hover' as const,
-                                position: 'right' as const,
-                            };
-
-                            delete childrenDropdownProps.filterDropdownVisible;
-
-                            item = renderDropdown(childrenDropdownProps, item, level + 1);
-                        }
-                        return item;
-                    })}
-            </Dropdown.Menu>
-        ),
+        render,
     };
 
     if (filterDropdownVisible != null) {
@@ -128,27 +131,75 @@ function renderDropdown(props: RenderDropdownProps = {}, nestedElem: React.React
     );
 }
 
-export interface ColumnFilterProps {
-    prefixCls?: string;
-    filteredValue?: any[];
-    filterIcon?: FilterIcon;
-    filterDropdown?: React.ReactElement;
-    renderFilterDropdown?: (props: RenderDropdownProps, options: { iconElem: React.ReactNode }) => React.ReactElement;
-    filterDropdownProps?: DropdownProps;
-    onFilterDropdownVisibleChange?: OnFilterDropdownVisibleChange;
-    onSelect?: (data: OnSelectData) => void
-}
-
 export default function ColumnFilter(props: ColumnFilterProps = {}): React.ReactElement {
     const {
         prefixCls = cssClasses.PREFIX,
         filteredValue,
         filterIcon = 'filter',
-        renderFilterDropdown,
         filterDropdownProps,
+        onSelect,
+        filterDropdownVisible,
+        renderFilterDropdown,
+        onFilterDropdownVisibleChange
     } = props;
     let { filterDropdown = null } = props;
 
+    
+    // custom filter related status
+    const isFilterDropdownVisibleControlled = typeof filterDropdownVisible !== 'undefined';
+    const isCustomFilterDropdown = typeof renderFilterDropdown === 'function';
+    const isCustomDropdownVisible = !isFilterDropdownVisibleControlled && isCustomFilterDropdown;
+    const [tempFilteredValue, setTempFilteredValue] = useState<any[]>(filteredValue);
+    const dropdownVisibleInitValue = isCustomDropdownVisible ? false : filterDropdownVisible;
+    const [dropdownVisible, setDropdownVisible] = useState<boolean | undefined>(dropdownVisibleInitValue);
+
+    useEffect(() => {
+        if (typeof filterDropdownVisible !== 'undefined') {
+            setDropdownVisible(filterDropdownVisible);
+        }
+    }, [filterDropdownVisible]);
+
+    useEffect(() => {
+        setTempFilteredValue(filteredValue);
+    }, [filteredValue]);
+
+    const confirm: RenderFilterDropdownProps['confirm'] = (props = {}) => {
+        const newFilteredValue = props?.filteredValue || tempFilteredValue;
+        if (!isEqual(newFilteredValue, filteredValue)) {
+            onSelect({ filteredValue: newFilteredValue });
+        }
+        if (props.closeDropdown) {
+            setDropdownVisible(false);
+        }
+    };
+
+    const clear: RenderFilterDropdownProps['clear'] = (props: { closeDropdown?: boolean } = {}) => {
+        setTempFilteredValue([]);
+        onSelect({ filteredValue: [] });
+        if (props.closeDropdown) {
+            setDropdownVisible(false);
+        }
+    };
+
+    const close: RenderFilterDropdownProps['close'] = () => {
+        setDropdownVisible(false);
+    };
+
+    const handleFilterDropdownVisibleChange = (visible: boolean) => {
+        if (isCustomDropdownVisible) {
+            setDropdownVisible(visible);
+        }
+        onFilterDropdownVisibleChange(visible);
+    };
+
+    const renderFilterDropdownProps: RenderFilterDropdownProps = {
+        tempFilteredValue,
+        setTempFilteredValue,
+        confirm,
+        clear,
+        close
+    };
+
     const finalCls = cls(`${prefixCls}-column-filter`, {
         on: Array.isArray(filteredValue) && filteredValue.length,
     });
@@ -177,32 +228,59 @@ export default function ColumnFilter(props: ColumnFilterProps = {}): React.React
     const renderProps = {
         ...props,
         ...filterDropdownProps,
+        ...renderFilterDropdownProps,
+        filterDropdownVisible: isFilterDropdownVisibleControlled ? filterDropdownVisible : dropdownVisible,
+        onFilterDropdownVisibleChange: handleFilterDropdownVisibleChange,
     };
 
     filterDropdown = React.isValidElement<ColumnFilterProps>(filterDropdown) ?
         filterDropdown :
-        typeof renderFilterDropdown === 'function' ?
-            renderFilterDropdown(renderProps, { iconElem }) :
-            renderDropdown(renderProps, iconElem);
+        renderDropdown(renderProps, iconElem);
 
     return filterDropdown;
 }
 
-export interface OnSelectData {
-    value: any;
-    filteredValue: any;
-    included: boolean;
-    domEvent: React.MouseEvent<HTMLElement>
+export interface ColumnFilterProps extends Omit<RenderDropdownProps, keyof RenderFilterDropdownProps> {
+    prefixCls?: string;
+    filteredValue?: any[];
+    filterIcon?: FilterIcon;
+    filterDropdown?: React.ReactElement;
+    filterDropdownProps?: FilterDropdownProps;
+    filters?: Filter[]
 }
 
-export interface RenderDropdownProps {
+export interface RenderDropdownProps extends FilterDropdownProps, RenderFilterDropdownProps {
     filterMultiple?: boolean;
     filters?: Filter[];
     filteredValue?: any[];
     filterDropdownVisible?: boolean;
     onSelect?: (data: OnSelectData) => void;
     onFilterDropdownVisibleChange?: OnFilterDropdownVisibleChange;
-    trigger?: Trigger;
-    position?: Position;
+    renderFilterDropdown?: (props?: RenderFilterDropdownProps) => React.ReactNode;
     renderFilterDropdownItem?: RenderFilterDropdownItem
+}
+
+export interface FilterDropdownProps extends Omit<DropdownProps, 'render' | 'onVisibleChange'> {}
+
+export interface OnSelectData {
+    value?: any;
+    /** only this value is used now  */
+    filteredValue: any;
+    included?: boolean;
+    domEvent?: React.MouseEvent<HTMLElement>
+}
+
+export interface RenderFilterDropdownProps {
+    /** temporary filteredValue  */
+    tempFilteredValue: any[];
+    /** set temporary filteredValue  */
+    setTempFilteredValue: (tempFilteredValue: any[]) => void;
+    /** set tempFilteredValue to filteredValue. You can also pass filteredValue to directly set the filteredValue  */
+    confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
+    /** clear tempFilteredValue and filteredValue  */
+    clear: (props?: { closeDropdown?: boolean }) => void;
+    /** close dropdown  */
+    close: () => void;
+    /** column filters  */
+    filters?: RenderDropdownProps['filters']
 }

+ 8 - 3
packages/semi-ui/table/Table.tsx

@@ -1007,7 +1007,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
       */
     addFnsInColumn = (column: ColumnProps = {}) => {
         const { prefixCls } = this.props;
-        if (column && (column.sorter || column.filters || column.useFullRender)) {
+        if (column && (column.sorter || column.filters || column.onFilter || column.useFullRender)) {
             let hasSorterOrFilter = false;
             const { dataIndex, title: rawTitle, useFullRender } = column;
             const curQuery = this.foundation.getQuery(dataIndex);
@@ -1054,11 +1054,16 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             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)) {
+            if (
+                (Array.isArray(column.filters) && column.filters.length) ||
+                isValidElement(column.filterDropdown) ||
+                typeof column.renderFilterDropdown === 'function'
+            ) {
+
                 const filter = (
                     <ColumnFilter
                         key={strings.DEFAULT_KEY_COLUMN_FILTER}
-                        {...curQuery}
+                        {...omit(curQuery, 'children')}
                         filteredValue={filteredValue}
                         onFilterDropdownVisibleChange={(visible: boolean) =>
                             this.foundation.toggleShowFilter(dataIndex, visible)

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

@@ -108,7 +108,9 @@ export {
     ShowHeader,
     KeepDOM,
     SortIcon,
-    FixedAllDisabledAndSelected
+    FixedAllDisabledAndSelected,
+    FeatRenderFilterDropdown,
+    InputFilter
 } from './v2';
 export { default as FixSelectAll325 } from './Demos/rowSelection';
 

+ 135 - 0
packages/semi-ui/table/_story/v2/FeatRenderFilterDropdown/index.tsx

@@ -0,0 +1,135 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Button, Space } from '@douyinfe/semi-ui';
+import type { ColumnProps } from '../../../interface';
+import * as dateFns from 'date-fns';
+
+/**
+ * test with cypress, please don't modify this story
+ */
+export default function App() {
+    const [dataSource, setData] = useState([]);
+    const inputRef = useRef<HTMLInputElement>();
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+    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>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = (value: any) => {
+                    const filteredValue = value ? [value] : [];
+                    setTempFilteredValue(filteredValue);
+                    // 你也可以在 input value 变化时直接筛选
+                    // confirm({ filteredValue });
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input ref={inputRef} value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: true })}>筛选+关闭</Button>
+                            <Button onClick={() => clear({ closeDropdown: true })}>清除+关闭</Button>
+                            <Button onClick={() => close()}>直接关闭</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+            onFilterDropdownVisibleChange: (visible) => {
+                console.log('inputRef', visible, inputRef);
+                inputRef.current?.focus?.();
+            }
+        },
+        {
+            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>
+                );
+            },
+            onFilter: (value, record) => record.owner.includes(value),
+            defaultFilteredValue: ['姜鹏志'],
+            renderFilterDropdown: (props) => {
+                console.log('renderFilterDropdown', props);
+                const { tempFilteredValue, setTempFilteredValue, confirm, clear, close } = props;
+
+                const handleChange = (value: any) => {
+                    if (value) {
+                        setTempFilteredValue([value]);
+                    } else {
+                        setTempFilteredValue([]);
+                    }
+                };
+
+                return (
+                    <Space vertical align='start' style={{ padding: 8 }}>
+                        <Input value={tempFilteredValue[0]} onChange={handleChange}/>
+                        <Space>
+                            <Button onClick={() => confirm({ closeDropdown: false })}>筛选后不关闭</Button>
+                            <Button onClick={() => clear({ closeDropdown: false })}>清除后不关闭</Button>
+                            <Button onClick={() => close()}>直接关闭</Button>
+                        </Space>
+                    </Space>
+                );
+            },
+        },
+        {
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}

+ 117 - 0
packages/semi-ui/table/_story/v2/InputFilter/index.tsx

@@ -0,0 +1,117 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Avatar, Input, Space } from '@douyinfe/semi-ui';
+import type { ColumnProps } from '../../../interface';
+import * as dateFns from 'date-fns';
+
+/**
+ * test with cypress, please don't modify this story
+ */
+export default function App() {
+    const [dataSource, setData] = useState([]);
+    const [filteredValue, setFilteredValue] = useState(['设计稿']);
+    const compositionRef = useRef({ isComposition: false });
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+    const handleChange = (value: string) => {
+        if (compositionRef.current?.isComposition) {
+            return;
+        }
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+    const handleCompositionStart = () => {
+        compositionRef.current.isComposition = true;
+    };
+
+    const handleCompositionEnd = (event: React.CompositionEvent) => {
+        compositionRef.current.isComposition = false;
+        const value = event.target?.value;
+        const newFilteredValue = value ? [value] : [];
+        setFilteredValue(newFilteredValue);
+    };
+
+
+    const columns: ColumnProps[] = [
+        {
+            title: (
+                <Space>
+                    <span>标题</span>
+                    <Input
+                        style={{ width: 200 }}
+                        defaultValue={filteredValue[0]}
+                        onCompositionStart={handleCompositionStart}
+                        onCompositionEnd={handleCompositionEnd}
+                        onChange={handleChange}
+                        showClear 
+                    />
+                </Space>
+            ),
+            dataIndex: 'name',
+            width: 400,
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                        {text}
+                    </div>
+                );
+            },
+            onFilter: (value, record) => record.name.includes(value),
+            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 = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table columns={columns} dataSource={dataSource} />;
+}

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

@@ -28,3 +28,5 @@ export { default as ShowHeader } from './ShowHeader';
 export { default as KeepDOM } from './KeepDOM';
 export { default as SortIcon } from './SortIcon';
 export { default as FixedAllDisabledAndSelected } from './FixedAllDisabledAndSelected';
+export { default as FeatRenderFilterDropdown } from './FeatRenderFilterDropdown';
+export { default as InputFilter } from './InputFilter';

+ 18 - 10
packages/semi-ui/table/interface.ts

@@ -1,13 +1,12 @@
 import React, { ReactNode, MutableRefObject } from 'react';
 
-import { BaseProps } from '../_base/baseComponent';
-import { PaginationProps } from '../pagination';
-import { CheckboxProps } from '../checkbox';
-import { DropdownProps } from '../dropdown';
-import { Locale } from '../locale/interface';
-import { ArrayElement } from '../_base/base';
+import type { BaseProps } from '../_base/baseComponent';
+import type { PaginationProps } from '../pagination';
+import type { CheckboxProps } from '../checkbox';
+import type { Locale } from '../locale/interface';
+import type { ArrayElement } from '../_base/base';
 import { strings } from '@douyinfe/semi-foundation/table/constants';
-import {
+import type {
     BaseRowKeyType,
     BaseSortOrder,
     BaseGroupBy,
@@ -21,7 +20,8 @@ import {
     BaseIncludeGroupRecord,
     BaseEllipsis
 } from '@douyinfe/semi-foundation/table/foundation';
-import { ScrollDirection, CSSDirection } from 'react-window';
+import type { ScrollDirection, CSSDirection } from 'react-window';
+import type { ColumnFilterProps } from './ColumnFilter';
 
 export interface TableProps<RecordType extends Record<string, any> = any> extends BaseProps {
     bordered?: boolean;
@@ -81,29 +81,37 @@ export interface ColumnProps<RecordType extends Record<string, any> = any> {
     children?: Array<ColumnProps<RecordType>>;
     className?: string;
     colSpan?: number;
+    /** use `dataIndex` to get current column data item from record. If you use `sorter` or `onFilter`, a unique `dataIndex` is required  */
     dataIndex?: string;
     defaultFilteredValue?: any[];
     defaultSortOrder?: SortOrder;
     filterChildrenRecord?: boolean;
-    filterDropdown?: React.ReactNode;
-    filterDropdownProps?: DropdownProps;
+    filterDropdown?: ColumnFilterProps['filterDropdown'];
+    /** render filter Dropdown panel content  */
+    renderFilterDropdown?: ColumnFilterProps['renderFilterDropdown'];
+    /** filter Dropdown props  */
+    filterDropdownProps?: ColumnFilterProps['filterDropdownProps'];
     filterDropdownVisible?: boolean;
     filterIcon?: FilterIcon;
     filterMultiple?: boolean;
     filteredValue?: any[];
+    /** `filters` is not required if you use `renderFilterDropdown`  */
     filters?: Filter[];
     fixed?: Fixed;
+    /** the key required by React. If you have already set the `dataIndex`, the key does not need to be set again.  */
     key?: string | number;
     render?: ColumnRender<RecordType>;
     renderFilterDropdownItem?: RenderFilterDropdownItem;
     sortChildrenRecord?: boolean;
     sortOrder?: SortOrder;
+    /** enable sorting, `dataIndex` is required at the same time  */
     sorter?: Sorter<RecordType>;
     sortIcon?: SortIcon;
     title?: ColumnTitle;
     useFullRender?: boolean;
     width?: string | number;
     onCell?: OnCell<RecordType>;
+    /** enable filtering, `dataIndex` is required at the same time  */
     onFilter?: OnFilter<RecordType>;
     onFilterDropdownVisibleChange?: OnFilterDropdownVisibleChange;
     onHeaderCell?: OnHeaderCell<RecordType>;