Browse Source

fix(table): remove measure scrollbar column and use overflow scroll to simulate scrollbar column (#164)

走鹃 4 years ago
parent
commit
27b3ad1eed

+ 2 - 2
content/show/table/index.md

@@ -821,8 +821,8 @@ render(CustomDropdownItem);
 
 > 注意:
 >
-> -   自 0.27.0版本后,展开按钮会默认与第一列文案渲染在同一个单元格内,你可以通过往Table传入 hideExpandedColumn={false} 将展开按钮单独作为一列渲染。
-> -   请务必为每行数据提供一个与其他行值不同的 "key",或者使用 rowKey 参数指定一个作为主键的属性名。
+> - 自 0.27.0版本后,展开按钮会默认与第一列文案渲染在同一个单元格内,你可以通过往Table传入 `hideExpandedColumn={false}` 将展开按钮单独作为一列渲染。
+> - 请务必为每行数据提供一个与其他行值不同的 `key`,或者使用 rowKey 参数指定一个作为主键的属性名。
 
 #### 一般可展开行
 

+ 1 - 11
packages/semi-foundation/table/bodyFoundation.ts

@@ -15,15 +15,6 @@ export default class TableBodyFoundation<P = Record<string, any>, S = Record<str
     init() {
         this.initVirtualizedData();
         this.initExpandBtnShouldInRow();
-
-        /**
-         * Use ResizeObserver to monitor changes in the size of the body content area, and notify Table to recalculate if it changes. columns #1219
-         * (Only monitor the scroll.y scene, other scenes are not monitored, because the header of the scroll.y scene is a separate table, and a scrollbar column will be inserted)
-         */
-        const { scroll } = this.getProps(); // TODO check: this._adapter.getProps -> this.getProps
-        if (get(scroll, 'y')) {
-            this.observeBodyResize();
-        }
     }
 
     destroy() {
@@ -120,9 +111,8 @@ export default class TableBodyFoundation<P = Record<string, any>, S = Record<str
     /**
      * Use ResizeObserver to monitor changes in the size of the body content area, and notify Table to recalculate if it changes. columns #1219
      * (Only monitor the scroll.y scene, other scenes are not monitored, because the header of the scroll.y scene is a separate table, and a scrollbar column will be inserted)
-     * @param {any} bodyDOM
      */
-    observeBodyResize(bodyDOM?: any) {
+    observeBodyResize(bodyDOM: any) {
         const { scroll } = this.getProps(); // TODO check: this._adapter.getProps -> this.getProps
         if (get(scroll, 'y')) {
             return this._adapter.observeBodyResize(bodyDOM);

+ 10 - 1
packages/semi-foundation/table/rtl.scss

@@ -112,6 +112,7 @@ $module: #{$prefix}-table;
         &-bordered {
             .#{$module}-container {
                 border-left: 0;
+                border-right: $border-table;
             }
 
             .#{$module}-thead > .#{$module}-row > .#{$module}-row-head,
@@ -121,9 +122,17 @@ $module: #{$prefix}-table;
             }
 
             .#{$module}-placeholder {
-                border-left: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
+                border-left: $border-table;
                 border-right: 0;
             }
+
+            .#{$module}-header {
+
+                &::-webkit-scrollbar {
+                    border-right: 0;
+                    border-left: $border-table;
+                }
+            }
         }
 
         &-fixed {

+ 17 - 3
packages/semi-foundation/table/table.scss

@@ -54,6 +54,12 @@ $module: #{$prefix}-table;
 
     &-header {
         overflow: hidden;
+
+        &::-webkit-scrollbar {
+            background-color: transparent;
+            border-bottom: $border-table_head-bottom;
+        }
+        scrollbar-base-color: transparent;
     }
 
     &-body {
@@ -83,7 +89,7 @@ $module: #{$prefix}-table;
                 color: $color-table_th-text-default;
                 font-weight: $font-weight-bold;
                 text-align: left;
-                border-bottom: #{$width-table_header_border} #{$border-table_base-borderStyle} $color-table_th-border-default;
+                border-bottom: $border-table_head-bottom;
                 padding-left: $spacing-table_row_head-paddingX;
                 padding-right: $spacing-table_row_head-paddingX;
                 padding-top: $spacing-table_row_head-paddingY;
@@ -420,11 +426,18 @@ $module: #{$prefix}-table;
         }
 
         .#{$module}-container {
-            border: #{$width-table_base_border} #{$border-table_base-borderStyle} $color-table-border-default;
+            border: $border-table;
             border-right: 0;
             border-bottom: 0;
         }
 
+        .#{$module}-header {
+
+            &::-webkit-scrollbar {
+                border-right: $border-table;
+            }
+        }
+
         .#{$module}-footer {
             border-left: $border-table;
             border-right: $border-table;
@@ -444,7 +457,7 @@ $module: #{$prefix}-table;
         }
 
         .#{$module}-placeholder {
-            border-right: $width-table_base_border $border-table_base-borderStyle $color-table-border-default;
+            border-right: $border-table;
         }
     }
 
@@ -462,6 +475,7 @@ $module: #{$prefix}-table;
 
     &-fixed {
         table-layout: fixed;
+        min-width: 100%;
 
         & > .#{$module}-tbody {
             & > .#{$module}-row-expand > .#{$module}-row-cell > .#{$module}-expand-inner,

+ 1 - 0
packages/semi-foundation/table/variables.scss

@@ -80,4 +80,5 @@ $border-table_base-borderStyle: solid; // 表格描边样式
 $shadow-table_left: -3px 0 0 0 $color-table_shadow-bg-default; // 表格滚动阴影 - 左侧
 $shadow-table_right: 3px 0 0 0 $color-table_shadow-bg-default; // 表格滚动阴影 - 右侧
 $border-table: #{$width-table_base_border} #{$border-table_base-borderStyle} $color-table-border-default; // 表格默认描边
+$border-table_head-bottom: #{$width-table_header_border} #{$border-table_base-borderStyle} $color-table_th-border-default; // 表头单元格描边 - 底部
 $border-table_resizer: $width-table_resizer_border solid $color-table_resizer-bg-default; // 表格拉伸标识描边

+ 5 - 2
packages/semi-ui/configProvider/_story/RTLDirection/RTLWrapper.jsx → packages/semi-ui/configProvider/_story/RTLDirection/RTLWrapper.tsx

@@ -1,11 +1,14 @@
 import React, { useState } from 'react';
 import { ButtonGroup, Button, ConfigProvider } from '@douyinfe/semi-ui';
 
-export default function RTLWrapper({ children, onDirectionChange }) {
+export default function RTLWrapper({ children, onDirectionChange }: { children: React.ReactNode; onDirectionChange?: (direction: 'ltr' | 'rtl') => void }) {
     const [direction, setDirection] = useState();
     const handleDirectionChange = dir => {
         setDirection(dir);
-        onDirectionChange(dir);
+        
+        if (typeof onDirectionChange === 'function') {
+            onDirectionChange(dir);
+        }
     };
 
     return (

+ 25 - 17
packages/semi-ui/table/Body/index.tsx

@@ -22,7 +22,7 @@ import { strings } from '@douyinfe/semi-foundation/table/constants';
 import Store from '@douyinfe/semi-foundation/utils/Store';
 
 import BaseComponent, { BaseProps } from '../../_base/baseComponent';
-import { measureScrollbar, logger } from '../utils';
+import { logger } from '../utils';
 import ColGroup from '../ColGroup';
 import BaseRow from './BaseRow';
 import ExpandedRow from './ExpandedRow';
@@ -126,6 +126,7 @@ class Body extends BaseComponent<BodyProps, BodyState> {
     ref: React.MutableRefObject<any>;
     listRef: React.MutableRefObject<any>;
     observer: ResizeObserver;
+    foundation: BodyFoundation;
     constructor(props: BodyProps, context: BodyContext) {
         super(props);
         this.ref = React.createRef();
@@ -159,7 +160,7 @@ class Body extends BaseComponent<BodyProps, BodyState> {
             setVirtualizedData: (virtualizedData, cb) => this.setState({ virtualizedData }, cb),
             setCachedExpandBtnShouldInRow: cachedExpandBtnShouldInRow => this.setState({ cachedExpandBtnShouldInRow }),
             setCachedExpandRelatedProps: cachedExpandRelatedProps => this.setState({ cachedExpandRelatedProps }),
-            observeBodyResize: bodyWrapDOM => {
+            observeBodyResize: (bodyWrapDOM: HTMLDivElement) => {
                 const { setBodyHasScrollbar } = this.context;
 
                 // Callback when the size of the body dom content changes, notifying Table.jsx whether the bodyHasScrollBar exists
@@ -176,11 +177,12 @@ class Body extends BaseComponent<BodyProps, BodyState> {
                 // Monitor body dom resize
                 if (bodyWrapDOM) {
                     if (get(window, 'ResizeObserver')) {
-                        // no need to observe many times when ref update
-                        if (!this.observer) {
-                            this.observer = new ResizeObserver(resizeCallback);
-                            this.observer.observe(bodyWrapDOM);
+                        if (this.observer) {
+                            this.observer.unobserve(bodyWrapDOM);
+                            this.observer = null;
                         }
+                        this.observer = new ResizeObserver(resizeCallback);
+                        this.observer.observe(bodyWrapDOM);
                     } else {
                         logger.warn(
                             'The current browser does not support ResizeObserver,' +
@@ -194,17 +196,19 @@ class Body extends BaseComponent<BodyProps, BodyState> {
                 const bodyWrapDOM = this.ref.current;
                 if (this.observer) {
                     this.observer.unobserve(bodyWrapDOM);
+                    this.observer = null;
                 }
             }
         };
     }
 
     componentDidUpdate(prevProps: BodyProps, prevState: BodyState) {
-        if (this.props.virtualized) {
+        const { virtualized, dataSource, expandedRowKeys, columns, scroll  } = this.props;
+        if (virtualized) {
             if (
-                prevProps.dataSource !== this.props.dataSource ||
-                prevProps.expandedRowKeys !== this.props.expandedRowKeys ||
-                prevProps.columns !== this.props.columns
+                prevProps.dataSource !== dataSource ||
+                prevProps.expandedRowKeys !== expandedRowKeys ||
+                prevProps.columns !== columns
             ) {
                 this.foundation.initVirtualizedData();
             }
@@ -215,6 +219,12 @@ class Body extends BaseComponent<BodyProps, BodyState> {
         if (!isEqual(newExpandRelatedProps, prevState.cachedExpandRelatedProps)) {
             this.foundation.initExpandBtnShouldInRow(newExpandRelatedProps);
         }
+
+        const scrollY = get(scroll, 'y');
+        const bodyWrapDOM = this.ref.current;
+        if (scrollY && scrollY !== get(prevProps, 'scroll.y')) {
+            this.foundation.observeBodyResize(bodyWrapDOM);
+        }
     }
 
     forwardRef = (node: HTMLDivElement) => {
@@ -289,12 +299,11 @@ class Body extends BaseComponent<BodyProps, BodyState> {
 
     getVirtualizedRowWidth = () => {
         const { getCellWidths } = this.context;
-        const { columns, anyColumnFixed } = this.props;
+        const { columns } = this.props;
         const cellWidths = getCellWidths(columns);
-        const measuredScrollbarWidth = measureScrollbar('vertical');
         const rowWidth = arrayAdd(cellWidths, 0, size(columns));
 
-        return !anyColumnFixed && measuredScrollbarWidth ? rowWidth - measuredScrollbarWidth : rowWidth;
+        return rowWidth;
     };
 
     renderVirtualizedRow = (options: { index?: number; style?: React.CSSProperties; isScrolling?: boolean }) => {
@@ -396,8 +405,6 @@ class Body extends BaseComponent<BodyProps, BodyState> {
             return null;
         }
 
-        const measuredScrollbarWidth = measureScrollbar('vertical');
-
         const rawY = get(scroll, 'y');
         const yIsNumber = typeof rawY === 'number';
         const y = yIsNumber ? rawY : 600;
@@ -409,8 +416,8 @@ class Body extends BaseComponent<BodyProps, BodyState> {
         const listStyle = {
             width: '100%',
             height: y,
-            overflowX: anyColumnFixed ? 'scroll' : 'auto',
-            overflowY: measuredScrollbarWidth ? 'scroll' : 'auto',
+            overflowX: 'auto',
+            overflowY: 'auto',
         } as const;
 
         const wrapCls = classnames(`${prefixCls}-body`);
@@ -433,6 +440,7 @@ class Body extends BaseComponent<BodyProps, BodyState> {
                 innerElementType={this.renderTbody}
                 outerElementType={this.renderOuter}
                 style={{ ...listStyle, direction }}
+                direction={direction}
             >
                 {this.renderVirtualizedRow}
             </List>

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

@@ -11,6 +11,7 @@ import { Fixed, TableComponents, Scroll, BodyScrollEvent, ColumnProps } from './
 export interface HeadTableProps {
     [x: string]: any;
     anyColumnFixed?: boolean;
+    bodyHasScrollBar?: boolean;
     columns?: ColumnProps[];
     components?: TableComponents;
     dataSource?: Record<string, any>[];
@@ -30,6 +31,7 @@ export interface HeadTableProps {
 class HeadTable extends React.PureComponent<HeadTableProps> {
     static propTypes = {
         anyColumnFixed: PropTypes.bool,
+        bodyHasScrollBar: PropTypes.bool,
         columns: PropTypes.array,
         components: PropTypes.object,
         dataSource: PropTypes.array,
@@ -37,7 +39,10 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
         handleBodyScroll: PropTypes.func,
         prefixCls: PropTypes.string,
         forwardedRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
-        scroll: PropTypes.object,
+        scroll: PropTypes.shape({
+            x: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]),
+            y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+        }),
         selectedRowKeysSet: PropTypes.instanceOf(Set).isRequired, // Useful when update is selected
         showHeader: PropTypes.bool,
         onDidUpdate: PropTypes.func,
@@ -63,6 +68,7 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
             onDidUpdate,
             showHeader,
             anyColumnFixed,
+            bodyHasScrollBar
         } = this.props;
 
         if (!showHeader) {
@@ -71,13 +77,17 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
 
         const Table = get(components, 'header.outer', 'table');
         const x = get(scroll, 'x');
-        const headStyle = {};
+        const headStyle: Partial<React.CSSProperties> = {};
         const tableStyle: { width?: number | string } = {};
 
         if (x && !fixed) {
             tableStyle.width = x;
         }
 
+        if (bodyHasScrollBar) {
+            headStyle.overflowY = 'scroll';
+        }
+
         const colgroup = <ColGroup columns={columns} prefixCls={prefixCls} />;
         const tableHeader = (
             <TableHeader {...this.props} columns={columns} components={components} onDidUpdate={onDidUpdate} />

+ 2 - 21
packages/semi-ui/table/Table.tsx

@@ -1081,7 +1081,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
                     key="head"
                     anyColumnFixed={anyColumnFixed}
                     ref={headerRef}
-                    columns={bodyHasScrollBar ? columns : filteredColumns}
+                    columns={filteredColumns}
                     prefixCls={prefixCls}
                     fixed={fixed}
                     handleBodyScroll={this.handleBodyScrollLeft}
@@ -1090,6 +1090,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
                     showHeader={showHeader}
                     selectedRowKeysSet={selectedRowKeysSet}
                     dataSource={dataSource}
+                    bodyHasScrollBar={bodyHasScrollBar}
                 />
             ) : null;
 
@@ -1187,24 +1188,6 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             }
         }
 
-        /**
-          * Whether to insert the scroll shaft in the header
-          * If there is no fixed but there is a vertical scroll axis, the scroll-bar should be inserted in the head
-          */
-        if (isAnyFixedRight(columns) || get(scroll, 'y')) {
-            const scrollbarWidth = measureScrollbar('vertical');
-            if (scrollbarWidth) {
-                const column = this.normalizeScrollbarColumn({ scrollbarWidth });
-
-                const destIndex = findIndex(columns, item => item.key === strings.DEFAULT_KEY_COLUMN_SCROLLBAR);
-                if (destIndex > -1) {
-                    columns[destIndex] = { ...column, ...columns[destIndex] };
-                } else {
-                    columns.push(column);
-                }
-            }
-        }
-
         assignColumnKeys(columns);
 
         return columns;
@@ -1281,13 +1264,11 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
           */
         if (!this.adapter.isAnyColumnUseFullRender(queries)) {
             const rowSelectionUpdate: boolean = propRowSelection && !get(propRowSelection, 'hidden');
-            const scrollbarColumnUpdate: boolean | string | number = isAnyFixedRight(cachedColumns) || get(scroll, 'y');
             columns = this.foundation.memoizedWithFnsColumns(
                 queries,
                 cachedColumns,
                 rowSelectionUpdate,
                 hideExpandedColumn,
-                scrollbarColumnUpdate,
                 // Update the columns after the body scrollbar changes to ensure that the head and body are aligned
                 bodyHasScrollBar
             );

+ 115 - 0
packages/semi-ui/table/_story/BetterScrollbar.tsx

@@ -0,0 +1,115 @@
+import React from 'react';
+import { Table, Tooltip, Tag, Space, Typography, Switch } from '../../index';
+import RTLWrapper from '../../configProvider/_story/RTLDirection/RTLWrapper';
+
+function App() {
+    const [bordered, setBordered] = React.useState(true);
+    const columns = [
+        {
+            title: 'Name',
+            dataIndex: 'name',
+            width: 150,
+            fixed: true,
+            filters: [
+                {
+                    text: 'Code 45',
+                    value: '45',
+                },
+                {
+                    text: 'King 4',
+                    value: 'King 4',
+                },
+            ],
+            onFilter: (value, record) => record.name.includes(value),
+        },
+        {
+            title: 'Age',
+            dataIndex: 'age',
+            fixed: true,
+            width: 150,
+            sorter: (a, b) => (a.age - b.age > 0 ? 1 : -1),
+        },
+        {
+            title: 'Address',
+            width: 200,
+            dataIndex: 'address',
+        },
+        // {
+        //     title: 'Address2',
+        //     width: 200,
+        //     dataIndex: 'address',
+        // },
+        // {
+        //     title: 'Address3',
+        //     width: 200,
+        //     dataIndex: 'address',
+        // },
+        // {
+        //     title: 'Address4',
+        //     width: 200,
+        //     dataIndex: 'address',
+        // },
+        // {
+        //     title: 'Address5',
+        //     width: 200,
+        //     dataIndex: 'address',
+        // },
+        {
+            fixed: 'right' as const,
+            width: 250,
+            render: (text, record) => (
+                <Tooltip content={record.description}>
+                    <Tag color="green">Show Info</Tag>
+                </Tooltip>
+            ),
+        },
+    ];
+    
+    const data = [];
+    
+    for (let i = 0; i < 10; i++) {
+        const age = 40 + (Math.random() > 0.5 ? 1 : -1) * (i % 9);
+        const name = `Edward King ${i}`;
+        data.push({
+            key: `${ i}`,
+            name,
+            age,
+            address: `London, Park Lane no. ${i}`,
+            description: `My name is ${name}, I am ${age} years old, living in New York No. ${i + 1} Lake Park.`,
+        });
+    }
+    
+    const allColumnsWidth = columns.reduce((count, column) => count + column.width, 0);
+
+    const tableProps = {
+        pagination: false,
+        columns,
+        bordered,
+        dataSource: data,
+    };
+
+    return (
+        <RTLWrapper>
+            <div style={{ textAlign: 'left', marginBottom: 12 }}>
+                <Space>
+                    <Typography.Title heading={6}>边框</Typography.Title>
+                    <Switch onChange={() => setBordered(!bordered)} checked={bordered} />
+                </Space>
+            </div>
+            <Table
+                {...tableProps}
+                scroll={{ y: 400 }}
+            />
+            <br />
+            <Table
+                {...tableProps}
+                title="虚拟化表格"
+                scroll={{ y: 400, x: allColumnsWidth }}
+                style={{ width: allColumnsWidth }}
+                virtualized
+            />
+        </RTLWrapper>
+    );
+}
+
+export default App;

+ 5 - 2
packages/semi-ui/table/_story/table.stories.tsx

@@ -1,11 +1,12 @@
 import React, { ReactNode, useMemo } from 'react';
 import { storiesOf } from '@storybook/react';
+import { IllustrationConstruction } from '@douyinfe/semi-illustrations';
+
 import { Fixed, ColumnProps, OnRow, OnHeaderRow, OnGroupedRow, RowKey } from '../interface';
 import JSXColumnsNest from './JSXColumnsNest';
 import DefaultSortOrder from './DefaultSortOrder';
+import BetterScrollbar from './BetterScrollbar';
 import Empty from '../../empty';
-import { IllustrationConstruction } from '@douyinfe/semi-illustrations';
-
 import Table from '../index';
 
 const stories = storiesOf('Table', module);
@@ -371,3 +372,5 @@ stories.add('empty', () => {
         <Table columns={columns} dataSource={[]} empty={test} />
     );
 });
+
+stories.add('better scrollbar', () => <BetterScrollbar />);