Browse Source

feat: table sticky

pointhalo 3 years ago
parent
commit
10ca7ddae7

+ 9 - 0
packages/semi-foundation/table/table.scss

@@ -64,6 +64,15 @@ $module: #{$prefix}-table;
             border-bottom: $width-table_header_border $border-table_base-borderStyle $color-table_th-border-default;
         }
         scrollbar-base-color: transparent;
+
+        &-sticky {
+            position: sticky;
+            z-index: $z-table_fixed_column + 1;
+
+            .semi-table-thead > .semi-table-row > .semi-table-row-head {
+                background-color: $color-table-bg-default;
+            }
+        }
     }
 
     &-body {

+ 12 - 2
packages/semi-ui/table/HeadTable.tsx

@@ -70,7 +70,8 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
             onDidUpdate,
             showHeader,
             anyColumnFixed,
-            bodyHasScrollBar
+            bodyHasScrollBar,
+            sticky
         } = this.props;
 
         if (!showHeader) {
@@ -95,11 +96,20 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
             <TableHeader {...this.props} columns={columns} components={components} onDidUpdate={onDidUpdate} />
         );
 
+        const headTableCls = classnames(`${prefixCls}-header`, {
+            [`${prefixCls}-header-sticky`]: sticky,
+        });
+
+        const stickyTop = get(sticky, 'top', 0);
+        if (typeof stickyTop === 'number') {
+            headStyle.top = stickyTop;
+        }
+
         return (
             <div
                 key="headTable"
                 style={headStyle}
-                className={`${prefixCls}-header`}
+                className={headTableCls}
                 ref={forwardedRef}
                 onScroll={handleBodyScroll}
             >

+ 7 - 1
packages/semi-ui/table/Table.tsx

@@ -258,12 +258,16 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             isAnyColumnFixed: (columns: ColumnProps<RecordType>[]) =>
                 some(this.getColumns(columns || this.props.columns, this.props.children), column => Boolean(column.fixed)),
             useFixedHeader: () => {
-                const { scroll } = this.props;
+                const { scroll, sticky } = this.props;
 
                 if (get(scroll, 'y')) {
                     return true;
                 }
 
+                if (sticky) {
+                    return true;
+                }
+
                 return false;
             },
             setHeadWidths: (headWidths: Array<BaseHeadWidth>, index = 0) => {
@@ -1099,6 +1103,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             dataSource,
             bodyHasScrollBar,
             disabledRowKeysSet,
+            sticky
         } = props;
         const selectedRowKeysSet = get(rowSelection, 'selectedRowKeysSet', new Set());
 
@@ -1119,6 +1124,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
                     onHeaderRow={onHeaderRow}
                     dataSource={dataSource}
                     bodyHasScrollBar={bodyHasScrollBar}
+                    sticky={sticky}
                 />
             ) : null;
 

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

@@ -9,4 +9,5 @@ export { default as FixedOnHeaderRow } from './FixedOnHeaderRow';
 export { default as RadioRowSelection } from './radioRowSelection';
 export { default as FixedVirtualizedEmpty } from './FixedVirtualizedEmpty';
 export { default as FixedFilter } from './FixedFilter';
-export { default as FixedSorter } from './FixedSorter';
+export { default as FixedSorter } from './FixedSorter';
+export { default as stickyHeaderTable } from './stickyHeader';

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

@@ -0,0 +1,3 @@
+body {
+    height: 150vh;
+}

+ 163 - 0
packages/semi-ui/table/_story/v2/stickyHeader/index.tsx

@@ -0,0 +1,163 @@
+import React, { useState, useMemo, useEffect } from 'react';
+// eslint-disable-next-line semi-design/no-import
+import { Table, Avatar, AvatarGroup } from '@douyinfe/semi-ui';
+import { IconMore } from '@douyinfe/semi-icons';
+import * as dateFns from 'date-fns';
+import './index.scss';
+
+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',
+        fixed: true,
+        width: 250,
+        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),
+    },
+    {
+        title: '大小',
+        dataIndex: 'size',
+        width: 200,
+        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
+        render: text => (
+            <div>
+                <AvatarGroup>
+                    <Avatar color="red" alt="Lisa LeBlanc">
+                        LL
+                    </Avatar>
+                    <Avatar alt="Caroline Xiao">CX</Avatar>
+                    <Avatar color="amber" alt="Rafal Matin">
+                        RM
+                    </Avatar>
+                </AvatarGroup>
+            </div>
+        ),
+    },
+    {
+        title: '所有者',
+        dataIndex: 'owner',
+        // width: 200,
+        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',
+        width: 200,
+        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
+        render: value => {
+            return dateFns.format(new Date(value), 'yyyy-MM-dd');
+        },
+    },
+    {
+        title: '',
+        dataIndex: 'operate',
+        fixed: 'right' as const,
+        align: 'center' as const,
+        width: 100,
+        render: () => {
+            return <IconMore />;
+        },
+    },
+];
+
+App.storyName = '固定表头';
+function App() {
+    const [dataSource, setData] = useState([]);
+
+    const scroll = useMemo(() => ({ x: 1200 }), []);
+    const rowSelection = useMemo(
+        () => ({
+            onChange: (selectedRowKeys, selectedRows) => {
+                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+            },
+            getCheckboxProps: record => ({
+                disabled: record.name === 'Michael James', // Table.Column configuration not to be checked
+                name: record.name,
+            }),
+            fixed: true,
+        }),
+        []
+    );
+
+    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 Pro 设计稿${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 (
+        <div style={{ height: '250vh' }}>
+            <div style={{ marginTop: 200 }}>
+                <h4>top = 100</h4>
+                <div>
+                    <Table
+                        columns={columns}
+                        dataSource={dataSource}
+                        rowSelection={rowSelection}
+                        scroll={scroll}
+                        sticky={{ top: 100 }}
+                    />
+                </div>
+            </div>
+            <div>
+                <h4>top = 100 + no fixed column</h4>
+                <div>
+                    <Table dataSource={dataSource} rowSelection={rowSelection} sticky={{ top: 100 }} scroll={scroll}>
+                        <Table.Column title="标题" dataIndex="name" key="name" />
+                        <Table.Column title="大小" dataIndex="size" key="size" />
+                        <Table.Column title="所有者" dataIndex="owner" key="owner" />
+                        <Table.Column title="更新时间" dataIndex="updateTime" key="updateTime" />
+                        <Table.Column title="" dataIndex="operate" key="operate" />
+                    </Table>
+                </div>
+            </div>
+        </div>
+    );
+}
+
+export default App;

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

@@ -70,6 +70,7 @@ export interface TableProps<RecordType extends Record<string, any> = any> extend
     onGroupedRow?: OnGroupedRow<RecordType>;
     onHeaderRow?: OnHeaderRow<RecordType>;
     onRow?: OnRow<RecordType>;
+    sticky?: Sticky;
 }
 
 export interface ColumnProps<RecordType extends Record<string, any> = any> {
@@ -318,4 +319,7 @@ export type BodyScrollPosition = 'both' | 'middle' | 'left' | 'right';
 
 export type TableLocale = Locale['Table'];
 export type Direction = CSSDirection;
-export type IncludeGroupRecord<RecordType> = BaseIncludeGroupRecord<RecordType>;
+export type IncludeGroupRecord<RecordType> = BaseIncludeGroupRecord<RecordType>;
+export type Sticky = boolean | {
+    top?: number;
+}