فهرست منبع

feat: Table fixed column support RTL and fixed align bug in RTL #1471 (#1474)

* feat: Table fixed column support RTL and fixed align bug in RTL #1471

* feat: Table add direction prop and update RTL code #1471

* docs: update Table direction supported version #1471

---------

Co-authored-by: shijia.me <[email protected]>
走鹃 2 سال پیش
والد
کامیت
50c7693797

+ 2 - 1
.storybook/base/base.js

@@ -18,7 +18,8 @@ function resolve(...dirs) {
  */
  */
 function getAddons() {
 function getAddons() {
     let addons = [
     let addons = [
-        '@storybook/addon-a11y',
+        // for performance reason, only open `@storybook/addon-a11y` when dev a11y
+        // '@storybook/addon-a11y',
         '@storybook/addon-toolbars',
         '@storybook/addon-toolbars',
     ];
     ];
     
     

+ 4 - 4
content/other/configprovider/index-en-US.md

@@ -71,7 +71,7 @@ Global configuration `direction` can change the text direction of components。`
 Special components:
 Special components:
 - Command call of Modal, Notification and Toast needs to be passed to 'direction' through prop.
 - Command call of Modal, Notification and Toast needs to be passed to 'direction' through prop.
 - If you want to internationalize the directional icon, you need to handle it on your own. We think RTL for icon will make it difficult to understand and maintain. Semi has adapted the icons in other components.
 - If you want to internationalize the directional icon, you need to handle it on your own. We think RTL for icon will make it difficult to understand and maintain. Semi has adapted the icons in other components.
-- Table fixed columns or headers, tree data, and virtualized tables do not support RTL at the moment, Slider does not support RTL at the moment.
+- The tree data of Table does not support RTL ([Chrome, Safari have different behave with Firefox](https://codesandbox.io/s/table-rtl-treedata-uy7gzl?file=/src/App.jsx)), and fixed column supports RTL in v2.32 version, Slider does not support RTL yet.
 
 
 ```jsx live=true dir="column" hideInDSM
 ```jsx live=true dir="column" hideInDSM
 import React, { useState } from 'react';
 import React, { useState } from 'react';
@@ -379,9 +379,9 @@ function Demo(props = {}) {
 
 
 ## API Reference
 ## API Reference
 
 
-| Properties | Instructions                                                                                                          | type           | Default |
-| ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- | ------- |
-| direction | Sets the direction of the text | `ltr`\| `rtl` | `ltr` | 
+| Properties | Instructions                   | type          | Default |
+|------------|--------------------------------|---------------|---------|
+| direction  | Sets the direction of the text | `ltr`\| `rtl` | `ltr`   |
 | getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative` | function():HTMLElement | () => document.body    |
 | getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative` | function():HTMLElement | () => document.body    |
 | locale     | Multi-language configuration, same as the [usage](/en-US/other/locale) of `locale` parameter in `LocaleProvider` | object         |         |
 | locale     | Multi-language configuration, same as the [usage](/en-US/other/locale) of `locale` parameter in `LocaleProvider` | object         |         |
 | timeZone   | [Time zone identifier](#Time_Zone_Identifier)                                                                         | string\|number |         |
 | timeZone   | [Time zone identifier](#Time_Zone_Identifier)                                                                         | string\|number |         |

+ 7 - 7
content/other/configprovider/index.md

@@ -74,7 +74,7 @@ function Demo(props = {}) {
 特殊组件:
 特殊组件:
 - Modal,Notification,Toast 的命令式调用需要通过 prop 传 `direction`。
 - Modal,Notification,Toast 的命令式调用需要通过 prop 传 `direction`。
 - 如果你想对有方向性的 Icon 做 RTL 国际化,需要自己单独进行处理。我们认为对 Icon 进行 RTL 会让它变得难以理解和维护。其他组件内的 icon Semi 已经做了 RTL 适配。
 - 如果你想对有方向性的 Icon 做 RTL 国际化,需要自己单独进行处理。我们认为对 Icon 进行 RTL 会让它变得难以理解和维护。其他组件内的 icon Semi 已经做了 RTL 适配。
-- Table 的固定列或表头,树形数据,虚拟化表格暂不支持 RTL,Slider 暂不支持 RTL
+- Table 的树形数据暂不支持 RTL([Chrome、Safari 浏览器表现与 Firefox 表现不同](https://codesandbox.io/s/table-rtl-treedata-uy7gzl?file=/src/App.jsx)),固定列在 v2.32 版本支持 RTL,Slider 暂不支持 RTL
 
 
 
 
 ```jsx live=true dir="column" hideInDSM
 ```jsx live=true dir="column" hideInDSM
@@ -381,12 +381,12 @@ function Demo(props = {}) {
 
 
 ## API 参考
 ## API 参考
 
 
-| 属性     | 说明                                                                                | 类型           | 默认值 |
-| -------- | ----------------------------------------------------------------------------------- | -------------- | ------ |
-| direction | 设置文本的方向 | `ltr`\| `rtl` | `ltr` | 
-| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` | function():HTMLElement | () => document.body  | 
-| locale   | 多语言配置,同`LocaleProvider`中`locale`参数的[用法](/zh-CN/other/locale#使用) | object         |        |
-| timeZone | [时区标识](#时区标识)                                                               | string\|number |        |
+| 属性              | 说明                                                                          | 类型                   | 默认值              |
+|-------------------|-----------------------------------------------------------------------------|------------------------|---------------------|
+| direction         | 设置文本的方向                                                                | `ltr`\| `rtl`          | `ltr`               |
+| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative`      | function():HTMLElement | () => document.body |
+| locale            | 多语言配置,同`LocaleProvider`中`locale`参数的[用法](/zh-CN/other/locale#使用) | object                 |                     |
+| timeZone          | [时区标识](#时区标识)                                                         | string\|number         |                     |
 
 
 
 
 ### 时区标识
 ### 时区标识

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

@@ -4751,6 +4751,7 @@ import { Table } from '@douyinfe/semi-ui';
 | dataIndex | The key corresponding to the column data in the data item. It is required when using sorter or filter. | string |  |
 | 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** |
 | 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** |
 | defaultSortOrder | The default value of sortOrder, one of 'ascend'\|'descend'\|false | boolean\| string | false | **1.31.0** |
+| direction | RTL, LTR direction, the default value is equal to ConfigProvider direction, you can configure the direction of the Table separately here | 'ltr' \| 'rtl' |  | **2.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** |
 | 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 |  |
 | 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 |  |
 | filterDropdownProps | Props passing to Dropdown, see more in [Dropdown API](/en-US/show/dropdown#Dropdown) | object |  |
 | filterDropdownProps | Props passing to Dropdown, see more in [Dropdown API](/en-US/show/dropdown#Dropdown) | object |  |
@@ -4886,6 +4887,12 @@ function Demo() {
 -   The new aria-colindex of the cell indicates which column the current grid belongs to, and the first column is 1
 -   The new aria-colindex of the cell indicates which column the current grid belongs to, and the first column is 1
 -   Added aria-label to column filter and sort buttons, and added aria-label attribute to row select buttons
 -   Added aria-label to column filter and sort buttons, and added aria-label attribute to row select buttons
 
 
+## RTL/LTR
+
+- RTL default value of Table is controlled by [ConfigProvider](/zh-CN/other/configprovider)
+- The align and fixed properties of the Table column will be automatically switched in RTL, left <-> right. The RTL function of fixed columns is supported in v2.31
+- Table tree data does not support RTL ([Chrome and Safari browsers behave differently from Firefox](https://codesandbox.io/s/table-rtl-treedata-uy7gzl?file=/src/App.jsx ))
+
 ## Content Guidelines
 ## Content Guidelines
 
 
 -   Table title
 -   Table title

+ 9 - 3
content/show/table/index.md

@@ -4602,6 +4602,7 @@ render(App);
 | defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效 | boolean | false |
 | defaultExpandAllRows | 默认是否展开所有行,动态加载数据时不生效 | boolean | false |
 | defaultExpandAllGroupRows | 默认是否展开分组行,动态加载数据时不生效 | boolean | false | **1.30.0** |
 | defaultExpandAllGroupRows | 默认是否展开分组行,动态加载数据时不生效 | boolean | false | **1.30.0** |
 | defaultExpandedRowKeys | 默认展开的行 key 数组,,动态加载数据时不生效 | Array<\*> | [] |
 | defaultExpandedRowKeys | 默认展开的行 key 数组,,动态加载数据时不生效 | Array<\*> | [] |
+| direction | RTL、LTR 方向,默认值等于 ConfigProvider direction,可在此单独配置 Table 的 direction | 'ltr' \| 'rtl' |  | **2.31.0** |
 | empty | 无数据时展示的内容 | ReactNode | '暂无数据' |
 | empty | 无数据时展示的内容 | ReactNode | '暂无数据' |
 | expandCellFixed | 展开图标所在列是否固定,与 Column 中的 fixed 取值相同 | boolean\|string | false |
 | expandCellFixed | 展开图标所在列是否固定,与 Column 中的 fixed 取值相同 | boolean\|string | false |
 | expandIcon | 自定义展开按钮,传 `false` 关闭默认的渲染 | boolean \| ReactNode<br/> \| (expanded: boolean) => ReactNode |  |
 | expandIcon | 自定义展开按钮,传 `false` 关闭默认的渲染 | boolean \| ReactNode<br/> \| (expanded: boolean) => ReactNode |  |
@@ -4750,7 +4751,7 @@ import { Table } from '@douyinfe/semi-ui';
 
 
 | 属性 | 说明 | 类型 | 默认值 | 版本 |
 | 属性 | 说明 | 类型 | 默认值 | 版本 |
 | --- | --- | --- | --- | --- |
 | --- | --- | --- | --- | --- |
-| align | 设置列的对齐方式 | 'left' \| 'right' \| 'center' | 'left' |
+| align | 设置列的对齐方式,在 RTL 时会自动切换 | 'left' \| 'right' \| 'center' | 'left' |
 | className | 列样式名 | string |  |
 | className | 列样式名 | string |  |
 | children | 表头合并时用于子列的设置 | Column[] |  |
 | children | 表头合并时用于子列的设置 | Column[] |  |
 | colSpan | 表头列合并,设置为 0 时,不渲染 | number |  |
 | colSpan | 表头列合并,设置为 0 时,不渲染 | number |  |
@@ -4765,7 +4766,7 @@ import { Table } from '@douyinfe/semi-ui';
 | filterMultiple | 是否多选 | boolean | true |
 | filterMultiple | 是否多选 | boolean | true |
 | filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | any[] |  |
 | filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | any[] |  |
 | filters | 表头的筛选菜单项 | Filter[] |  |
 | filters | 表头的筛选菜单项 | Filter[] |  |
-| fixed | 列是否固定,可选 true(等效于 left) 'left' 'right' | boolean\|string | false |
+| fixed | 列是否固定,可选 true(等效于 left) 'left' 'right',在 RTL 时会自动切换 | boolean\|string | false |
 | key | React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 | string |  |
 | key | React 需要的 key,如果已经设置了唯一的 dataIndex,可以忽略这个属性 | string |  |
 | render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并 | (text: any, record: RecordType, index: number, { expandIcon?: ReactNode, selection?: ReactNode, indentText?: ReactNode }) => object\|ReactNode |  |
 | render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并 | (text: any, record: RecordType, index: number, { expandIcon?: ReactNode, selection?: ReactNode, indentText?: ReactNode }) => object\|ReactNode |  |
 | renderFilterDropdownItem | 自定义每个筛选项渲染方式,用法详见[自定义筛选项渲染](#自定义筛选项渲染) | ({ value: any, text: any, onChange: Function, level: number, ...otherProps }) => ReactNode | - | **1.1.0** |
 | renderFilterDropdownItem | 自定义每个筛选项渲染方式,用法详见[自定义筛选项渲染](#自定义筛选项渲染) | ({ value: any, text: any, onChange: Function, level: number, ...otherProps }) => ReactNode | - | **1.1.0** |
@@ -4895,6 +4896,12 @@ function Demo() {
 -   单元格的新增了 aria-colindex 表示当前格子属于第几列,第一列为 1
 -   单元格的新增了 aria-colindex 表示当前格子属于第几列,第一列为 1
 -   列的筛选和排序按钮添加了 aria-label,行的选择按钮添加了 aria-label 属性
 -   列的筛选和排序按钮添加了 aria-label,行的选择按钮添加了 aria-label 属性
 
 
+## RTL/LTR
+
+- Table 的 RTL 默认值为 [ConfigProvider](/zh-CN/other/configprovider) direction,可以通过 Table direction 覆盖
+- Table 列的 align 与 fixed 属性会在 RTL 时会自动切换,left <-> right,固定列的 RTL 功能于 v2.31 版本支持
+- Table 的树形数据暂不支持 RTL([Chrome、Safari 浏览器表现与 Firefox 表现不同](https://codesandbox.io/s/table-rtl-treedata-uy7gzl?file=/src/App.jsx))
+
 ## 文案规范
 ## 文案规范
 
 
 -   表格标题
 -   表格标题
@@ -4908,7 +4915,6 @@ function Demo() {
     -   列标题使用句子大小写;
     -   列标题使用句子大小写;
 -   表格操作
 -   表格操作
     -   可以遵循 [Button 的文案规范](/zh-CN/input/button#%E6%96%87%E6%A1%88%E8%A7%84%E8%8C%83)
     -   可以遵循 [Button 的文案规范](/zh-CN/input/button#%E6%96%87%E6%A1%88%E8%A7%84%E8%8C%83)
-
 ## 设计变量
 ## 设计变量
 
 
 <DesignToken/>
 <DesignToken/>

+ 33 - 2
packages/semi-foundation/table/rtl.scss

@@ -1,7 +1,6 @@
 $module: #{$prefix}-table;
 $module: #{$prefix}-table;
 
 
-.#{$prefix}-rtl,
-.#{$prefix}-portal-rtl {
+.#{$module}-wrapper-rtl {
     .#{$module} {
     .#{$module} {
         direction: rtl;
         direction: rtl;
         text-align: right;
         text-align: right;
@@ -148,6 +147,34 @@ $module: #{$prefix}-table;
                 }
                 }
             }
             }
         }
         }
+
+        &-scroll {
+            &-position {
+                &-left {
+                    .#{$module}-tbody,
+                    .#{$module}-thead {
+                        & > .#{$module}-row > .#{$module}-cell-fixed-left-last {
+                            box-shadow: $shadow-table_right;
+                        }
+                        & > .#{$module}-row > .#{$module}-cell-fixed-right-first {
+                            box-shadow: none;
+                        }
+                    }
+                }
+    
+                &-right {
+                    .#{$module}-tbody,
+                    .#{$module}-thead {
+                        & > .#{$module}-row > .#{$module}-cell-fixed-left-last {
+                            box-shadow: none;
+                        }
+                        & > .#{$module}-row > .#{$module}-cell-fixed-right-first {
+                            box-shadow: $shadow-table_left;
+                        }
+                    }
+                }
+            }
+        }
     }
     }
 
 
     .#{$module}-expand-icon {
     .#{$module}-expand-icon {
@@ -155,4 +182,8 @@ $module: #{$prefix}-table;
         margin-left: $spacing-table_expand_icon-marginRight;
         margin-left: $spacing-table_expand_icon-marginRight;
         transform: scaleX(-1) translateY(2px);
         transform: scaleX(-1) translateY(2px);
     }
     }
+
+    .#{$prefix}-spin {
+        direction: rtl;
+    }
 }
 }

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

@@ -28,6 +28,14 @@ $module: #{$prefix}-table;
         &[data-column-fixed='true'] {
         &[data-column-fixed='true'] {
             z-index: 1;
             z-index: 1;
         }
         }
+
+        &-ltr {
+            direction: ltr;
+
+            .#{$prefix}-spin {
+                direction: ltr;
+            }
+        }
     }
     }
 
 
     &-middle {
     &-middle {

+ 14 - 0
packages/semi-foundation/table/utils.ts

@@ -502,4 +502,18 @@ export function isTreeTable({ dataSource, childrenRecordName = 'children' }: { d
         }
         }
     }
     }
     return flag;
     return flag;
+}
+
+export function getRTLAlign(align: typeof strings.ALIGNS[number], direction?: 'ltr' | 'rtl'): typeof strings.ALIGNS[number] {
+    if (direction === 'rtl') {
+        switch (align) {
+            case 'left':
+                return 'right';
+            case 'right':
+                return 'left';
+            default:
+                return align;
+        }
+    }
+    return align;
 }
 }

+ 7 - 6
packages/semi-ui/configProvider/_story/RTLDirection/RTLWrapper.tsx

@@ -1,27 +1,28 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
-import { ButtonGroup, Button, ConfigProvider } from '@douyinfe/semi-ui';
+import { ButtonGroup, Button, ConfigProvider } from '../../../index';
 
 
-export default function RTLWrapper({ children, onDirectionChange }: { children: React.ReactNode; onDirectionChange?: (direction: 'ltr' | 'rtl') => void }) {
-    const [direction, setDirection] = useState();
+export default function RTLWrapper({ children, onDirectionChange, defaultDirection = 'ltr' }: { defaultDirection?: 'ltr' | 'rtl'; children: React.ReactNode; onDirectionChange?: (direction: 'ltr' | 'rtl') => void }) {
+    const [direction, setDirection] = useState(defaultDirection);
     const handleDirectionChange = dir => {
     const handleDirectionChange = dir => {
         setDirection(dir);
         setDirection(dir);
-        
+
         if (typeof onDirectionChange === 'function') {
         if (typeof onDirectionChange === 'function') {
             onDirectionChange(dir);
             onDirectionChange(dir);
         }
         }
     };
     };
 
 
     return (
     return (
-        <>
+        <div style={{ width: '100%' }}>
             <div style={{ marginBottom: 20 }}>
             <div style={{ marginBottom: 20 }}>
                 <ButtonGroup>
                 <ButtonGroup>
                     <Button onClick={() => handleDirectionChange('ltr')}>LTR</Button>
                     <Button onClick={() => handleDirectionChange('ltr')}>LTR</Button>
                     <Button onClick={() => handleDirectionChange('rtl')}>RTL</Button>
                     <Button onClick={() => handleDirectionChange('rtl')}>RTL</Button>
                 </ButtonGroup>
                 </ButtonGroup>
+                {`direction=${direction}`}
             </div>
             </div>
             <ConfigProvider direction={direction}>
             <ConfigProvider direction={direction}>
                 {children}
                 {children}
             </ConfigProvider>
             </ConfigProvider>
-        </>
+        </div>
     );
     );
 }
 }

+ 2 - 5
packages/semi-ui/table/Body/index.tsx

@@ -823,11 +823,8 @@ class Body extends BaseComponent<BodyProps, BodyState> {
 
 
     render() {
     render() {
         const { virtualized } = this.props;
         const { virtualized } = this.props;
-        return (
-            <ConfigContext.Consumer>
-                {({ direction }: { direction?: Direction }) => (virtualized ? this.renderVirtualizedBody(direction) : this.renderBody(direction))}
-            </ConfigContext.Consumer>
-        );
+        const { direction } = this.context;
+        return virtualized ? this.renderVirtualizedBody(direction) : this.renderBody(direction);
     }
     }
 }
 }
 
 

+ 11 - 5
packages/semi-ui/table/Table.tsx

@@ -56,7 +56,7 @@ import ColumnSorter from './ColumnSorter';
 import ExpandedIcon from './CustomExpandIcon';
 import ExpandedIcon from './CustomExpandIcon';
 import HeadTable, { HeadTableProps } from './HeadTable';
 import HeadTable, { HeadTableProps } from './HeadTable';
 import BodyTable, { BodyProps } from './Body';
 import BodyTable, { BodyProps } from './Body';
-import { measureScrollbar, logger, cloneDeep, mergeComponents } from './utils';
+import { logger, cloneDeep, mergeComponents } from './utils';
 import {
 import {
     ColumnProps,
     ColumnProps,
     TablePaginationProps,
     TablePaginationProps,
@@ -73,6 +73,7 @@ import {
     Data
     Data
 } from './interface';
 } from './interface';
 import { ArrayElement } from '../_base/base';
 import { ArrayElement } from '../_base/base';
+import ConfigContext from '../configProvider/context';
 
 
 export type NormalTableProps<RecordType extends Record<string, any> = Data> = Omit<TableProps<RecordType>, 'resizable'>;
 export type NormalTableProps<RecordType extends Record<string, any> = Data> = Omit<TableProps<RecordType>, 'resizable'>;
 
 
@@ -790,8 +791,9 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
         const node = this.bodyWrapRef.current;
         const node = this.bodyWrapRef.current;
         if (node && node.children && node.children.length) {
         if (node && node.children && node.children.length) {
             const scrollToLeft = node.scrollLeft === 0;
             const scrollToLeft = node.scrollLeft === 0;
+            // why use Math.abs? @see https://bugzilla.mozilla.org/show_bug.cgi?id=1447743
             const scrollToRight =
             const scrollToRight =
-                node.scrollLeft + 1 >=
+                Math.abs(node.scrollLeft) + 1 >=
                 node.children[0].getBoundingClientRect().width - node.getBoundingClientRect().width;
                 node.children[0].getBoundingClientRect().width - node.getBoundingClientRect().width;
             if (scrollToLeft && scrollToRight) {
             if (scrollToLeft && scrollToRight) {
                 this.setScrollPosition('both');
                 this.setScrollPosition('both');
@@ -1409,18 +1411,22 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
         return (
         return (
             <div
             <div
                 ref={this.rootWrapRef}
                 ref={this.rootWrapRef}
-                className={classnames(className, `${prefixCls}-wrapper`)}
+                className={classnames(className, `${prefixCls}-wrapper`, `${prefixCls}-wrapper-${props.direction}`)}
                 data-column-fixed={anyColumnFixed}
                 data-column-fixed={anyColumnFixed}
                 style={wrapStyle}
                 style={wrapStyle}
                 id={id}
                 id={id}
             >
             >
-                <TableContextProvider {...tableContextValue}>
+                <TableContextProvider {...tableContextValue} direction={props.direction}>
                     <Spin spinning={loading} size="large">
                     <Spin spinning={loading} size="large">
                         <div ref={this.wrapRef} className={wrapCls}>
                         <div ref={this.wrapRef} className={wrapCls}>
                             <React.Fragment key={'pagination-top'}>
                             <React.Fragment key={'pagination-top'}>
                                 {['top', 'both'].includes(paginationPosition) ? tablePagination : null}
                                 {['top', 'both'].includes(paginationPosition) ? tablePagination : null}
                             </React.Fragment>
                             </React.Fragment>
-                            {this.renderTitle({ title: (props as any).title, dataSource: props.dataSource, prefixCls: props.prefixCls })}
+                            {this.renderTitle({
+                                title: (props as any).title,
+                                dataSource: props.dataSource,
+                                prefixCls: props.prefixCls,
+                            })}
                             <div className={`${prefixCls}-container`}>{this.renderMainTable({ ...props })}</div>
                             <div className={`${prefixCls}-container`}>{this.renderMainTable({ ...props })}</div>
                             <React.Fragment key={'pagination-bottom'}>
                             <React.Fragment key={'pagination-bottom'}>
                                 {['bottom', 'both'].includes(paginationPosition) ? tablePagination : null}
                                 {['bottom', 'both'].includes(paginationPosition) ? tablePagination : null}

+ 28 - 9
packages/semi-ui/table/TableCell.tsx

@@ -7,12 +7,12 @@ import { get, noop, set, omit, isEqual, merge } from 'lodash';
 
 
 import { cssClasses, numbers } from '@douyinfe/semi-foundation/table/constants';
 import { cssClasses, numbers } from '@douyinfe/semi-foundation/table/constants';
 import TableCellFoundation, { TableCellAdapter } from '@douyinfe/semi-foundation/table/cellFoundation';
 import TableCellFoundation, { TableCellAdapter } from '@douyinfe/semi-foundation/table/cellFoundation';
-import { isSelectionColumn, isExpandedColumn } from '@douyinfe/semi-foundation/table/utils';
+import { isSelectionColumn, isExpandedColumn, getRTLAlign } from '@douyinfe/semi-foundation/table/utils';
 
 
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import Context, { TableContextProps } from './table-context';
 import Context, { TableContextProps } from './table-context';
 import { amendTableWidth } from './utils';
 import { amendTableWidth } from './utils';
-import { Align, ColumnProps, ExpandIcon } from './interface';
+import { ColumnProps, ExpandIcon } from './interface';
 
 
 export interface TableCellProps extends BaseProps {
 export interface TableCellProps extends BaseProps {
     record?: Record<string, any>;
     record?: Record<string, any>;
@@ -174,14 +174,16 @@ export default class TableCell extends BaseComponent<TableCellProps, Record<stri
 
 
         let tdProps: { style?: Partial<React.CSSProperties> } = {};
         let tdProps: { style?: Partial<React.CSSProperties> } = {};
         let customCellProps = {};
         let customCellProps = {};
+        const { direction } = this.context;
+        const isRTL = direction === 'rtl';
 
 
         const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
         const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
         const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
         const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
 
 
         if (fixedLeftFlag) {
         if (fixedLeftFlag) {
-            set(tdProps, 'style.left', typeof fixedLeft === 'number' ? fixedLeft : 0);
+            set(tdProps, isRTL ? 'style.right' : 'style.left', typeof fixedLeft === 'number' ? fixedLeft : 0);
         } else if (fixedRightFlag) {
         } else if (fixedRightFlag) {
-            set(tdProps, 'style.right', typeof fixedRight === 'number' ? fixedRight : 0);
+            set(tdProps, isRTL ? 'style.left' : 'style.right', typeof fixedRight === 'number' ? fixedRight : 0);
         }
         }
 
 
         if (width != null) {
         if (width != null) {
@@ -201,7 +203,8 @@ export default class TableCell extends BaseComponent<TableCellProps, Record<stri
         }
         }
 
 
         if (column.align) {
         if (column.align) {
-            tdProps.style = { ...tdProps.style, textAlign: column.align as Align };
+            const textAlign = getRTLAlign(column.align, direction);
+            tdProps.style = { ...tdProps.style, textAlign };
         }
         }
 
 
         return { tdProps, customCellProps };
         return { tdProps, customCellProps };
@@ -320,6 +323,8 @@ export default class TableCell extends BaseComponent<TableCellProps, Record<stri
             firstFixedRight,
             firstFixedRight,
             colIndex
             colIndex
         } = this.props;
         } = this.props;
+        const { direction } = this.context;
+        const isRTL = direction === 'rtl';
         const { className } = column;
         const { className } = column;
         const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
         const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
         const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
         const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
@@ -339,15 +344,29 @@ export default class TableCell extends BaseComponent<TableCellProps, Record<stri
 
 
         const inner = this.renderInner(text, indentText, realExpandIcon);
         const inner = this.renderInner(text, indentText, realExpandIcon);
 
 
+        let isFixedLeft, isFixedLeftLast, isFixedRight, isFixedRightFirst;
+
+        if (isRTL) {
+            isFixedLeft = fixedRightFlag;
+            isFixedLeftLast = firstFixedRight;
+            isFixedRight = fixedLeftFlag;
+            isFixedRightFirst = lastFixedLeft;
+        } else {
+            isFixedLeft = fixedLeftFlag;
+            isFixedLeftLast = lastFixedLeft;
+            isFixedRight = fixedRightFlag;
+            isFixedRightFirst = firstFixedRight;
+        }
+
         const columnCls = classnames(
         const columnCls = classnames(
             className,
             className,
             `${prefixCls}-row-cell`,
             `${prefixCls}-row-cell`,
             get(customCellProps, 'className'),
             get(customCellProps, 'className'),
             {
             {
-                [`${prefixCls}-cell-fixed-left`]: fixedLeftFlag,
-                [`${prefixCls}-cell-fixed-left-last`]: lastFixedLeft,
-                [`${prefixCls}-cell-fixed-right`]: fixedRightFlag,
-                [`${prefixCls}-cell-fixed-right-first`]: firstFixedRight,
+                [`${prefixCls}-cell-fixed-left`]: isFixedLeft,
+                [`${prefixCls}-cell-fixed-left-last`]: isFixedLeftLast,
+                [`${prefixCls}-cell-fixed-right`]: isFixedRight,
+                [`${prefixCls}-cell-fixed-right-first`]: isFixedRightFirst,
             }
             }
         );
         );
 
 

+ 3 - 0
packages/semi-ui/table/TableContextProvider.tsx

@@ -15,6 +15,7 @@ const TableContextProvider = ({
     renderSelection,
     renderSelection,
     getVirtualizedListRef,
     getVirtualizedListRef,
     setBodyHasScrollbar,
     setBodyHasScrollbar,
+    direction
 }: TableContextProps) => {
 }: TableContextProps) => {
     const tableContextValue = useMemo(
     const tableContextValue = useMemo(
         () => ({
         () => ({
@@ -30,6 +31,7 @@ const TableContextProvider = ({
             handleRowExpanded,
             handleRowExpanded,
             getVirtualizedListRef,
             getVirtualizedListRef,
             setBodyHasScrollbar,
             setBodyHasScrollbar,
+            direction
         }),
         }),
         [
         [
             anyColumnFixed,
             anyColumnFixed,
@@ -44,6 +46,7 @@ const TableContextProvider = ({
             handleRowExpanded,
             handleRowExpanded,
             getVirtualizedListRef,
             getVirtualizedListRef,
             setBodyHasScrollbar,
             setBodyHasScrollbar,
+            direction
         ]
         ]
     );
     );
 
 

+ 31 - 13
packages/semi-ui/table/TableHeaderRow.tsx

@@ -11,7 +11,8 @@ import {
     isLastLeftFixed,
     isLastLeftFixed,
     isFixedLeft,
     isFixedLeft,
     isFixedRight,
     isFixedRight,
-    sliceColumnsByLevel
+    sliceColumnsByLevel,
+    getRTLAlign
 } from '@douyinfe/semi-foundation/table/utils';
 } from '@douyinfe/semi-foundation/table/utils';
 import BaseComponent from '../_base/baseComponent';
 import BaseComponent from '../_base/baseComponent';
 import TableContext, { TableContextProps } from './table-context';
 import TableContext, { TableContextProps } from './table-context';
@@ -65,7 +66,7 @@ export default class TableHeaderRow extends BaseComponent<TableHeaderRowProps, R
 
 
     headerNode: HTMLElement;
     headerNode: HTMLElement;
     context: TableContextProps;
     context: TableContextProps;
-    
+
     constructor(props: TableHeaderRowProps) {
     constructor(props: TableHeaderRowProps) {
         super(props);
         super(props);
         this.headerNode = null;
         this.headerNode = null;
@@ -100,7 +101,8 @@ export default class TableHeaderRow extends BaseComponent<TableHeaderRowProps, R
 
 
     render() {
     render() {
         const { components, row, prefixCls, onHeaderRow, index, style, columns } = this.props;
         const { components, row, prefixCls, onHeaderRow, index, style, columns } = this.props;
-        const { getCellWidths } = this.context;
+        const { getCellWidths, direction } = this.context;
+        const isRTL = direction === 'rtl';
         const slicedColumns = sliceColumnsByLevel(columns, index);
         const slicedColumns = sliceColumnsByLevel(columns, index);
         const headWidths = getCellWidths(slicedColumns);
         const headWidths = getCellWidths(slicedColumns);
 
 
@@ -116,22 +118,36 @@ export default class TableHeaderRow extends BaseComponent<TableHeaderRowProps, R
                 typeof column.onHeaderCell === 'function' ? column.onHeaderCell(column, cellIndex, index) : {};
                 typeof column.onHeaderCell === 'function' ? column.onHeaderCell(column, cellIndex, index) : {};
             let cellStyle = { ...customProps.style };
             let cellStyle = { ...customProps.style };
             if (column.align) {
             if (column.align) {
-                cellStyle = { ...cellStyle, textAlign: column.align };
+                const textAlign = getRTLAlign(column.align, direction);
+                cellStyle = { ...cellStyle, textAlign };
                 customProps.className = classnames(customProps.className, column.className, {
                 customProps.className = classnames(customProps.className, column.className, {
-                    [`${prefixCls}-align-${column.align}`]: Boolean(column.align),
+                    [`${prefixCls}-align-${textAlign}`]: Boolean(textAlign),
                 });
                 });
             }
             }
 
 
+            let fixedLeft, fixedRight, fixedLeftLast, fixedRightFirst;
+            if (isRTL) {
+                fixedLeft = isFixedRight(column);
+                fixedRight = isFixedLeft(column);
+                fixedLeftLast = isFirstFixedRight(slicedColumns, column);
+                fixedRightFirst = isLastLeftFixed(slicedColumns, column);
+            } else {
+                fixedLeft = isFixedLeft(column);
+                fixedRight = isFixedRight(column);
+                fixedLeftLast = isLastLeftFixed(slicedColumns, column);
+                fixedRightFirst = isFirstFixedRight(slicedColumns, column);
+            }
+
             customProps.className = classnames(
             customProps.className = classnames(
                 `${prefixCls}-row-head`,
                 `${prefixCls}-row-head`,
                 column.className,
                 column.className,
                 customProps.className,
                 customProps.className,
                 // `${prefixCls}-fixed-columns`,
                 // `${prefixCls}-fixed-columns`,
                 {
                 {
-                    [`${prefixCls}-cell-fixed-left`]: isFixedLeft(column),
-                    [`${prefixCls}-cell-fixed-left-last`]: isLastLeftFixed(slicedColumns, column),
-                    [`${prefixCls}-cell-fixed-right`]: isFixedRight(column),
-                    [`${prefixCls}-cell-fixed-right-first`]: isFirstFixedRight(slicedColumns, column),
+                    [`${prefixCls}-cell-fixed-left`]: fixedLeft,
+                    [`${prefixCls}-cell-fixed-left-last`]: fixedLeftLast,
+                    [`${prefixCls}-cell-fixed-right`]: fixedRight,
+                    [`${prefixCls}-cell-fixed-right-first`]: fixedRightFirst,
                 }
                 }
             );
             );
 
 
@@ -142,16 +158,18 @@ export default class TableHeaderRow extends BaseComponent<TableHeaderRowProps, R
                 );
                 );
                 if (indexOfSlicedColumns > -1) {
                 if (indexOfSlicedColumns > -1) {
                     if (isFixedLeft(column)) {
                     if (isFixedLeft(column)) {
+                        const xPositionKey = isRTL ? 'right' : 'left';
                         cellStyle = {
                         cellStyle = {
                             ...cellStyle,
                             ...cellStyle,
                             position: 'sticky',
                             position: 'sticky',
-                            left: arrayAdd(headWidths, 0, indexOfSlicedColumns),
+                            [xPositionKey]: arrayAdd(headWidths, 0, indexOfSlicedColumns),
                         };
                         };
                     } else if (isFixedRight(column)) {
                     } else if (isFixedRight(column)) {
+                        const xPositionKey = isRTL ? 'left' : 'right';
                         cellStyle = {
                         cellStyle = {
                             ...cellStyle,
                             ...cellStyle,
                             position: 'sticky',
                             position: 'sticky',
-                            right: arrayAdd(headWidths, indexOfSlicedColumns + 1),
+                            [xPositionKey]: arrayAdd(headWidths, indexOfSlicedColumns + 1),
                         };
                         };
                     }
                     }
                 }
                 }
@@ -175,8 +193,8 @@ export default class TableHeaderRow extends BaseComponent<TableHeaderRowProps, R
                     role="columnheader"
                     role="columnheader"
                     aria-colindex={cellIndex + 1}
                     aria-colindex={cellIndex + 1}
                     {...props}
                     {...props}
-                    style={cellStyle} 
-                    key={column.key || column.dataIndex || cellIndex} 
+                    style={cellStyle}
+                    key={column.key || column.dataIndex || cellIndex}
                 />
                 />
             );
             );
         });
         });

+ 24 - 0
packages/semi-ui/table/_story/RTL/ColumnAlign.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { Space } from '../../../index';
+import ColumnAlign from '../v2/columnAlign';
+import RTLWrapper from '../../../configProvider/_story/RTLDirection/RTLWrapper';
+
+function App() {
+    return (
+        <Space vertical align='start' style={{ width: 800 }}>
+            <RTLWrapper defaultDirection='rtl'>
+                <ColumnAlign />
+            </RTLWrapper>
+            <RTLWrapper defaultDirection='ltr'>
+                <ColumnAlign />
+            </RTLWrapper>
+        </Space>
+    );
+}
+
+App.storyName = 'column align';
+App.parameters = {
+    chromatic: { disableSnapshot: false },
+};
+
+export default App;

+ 31 - 0
packages/semi-ui/table/_story/RTL/Direction.jsx

@@ -0,0 +1,31 @@
+import React, { useState } from 'react';
+import ColumnAlign from '../v2/columnAlign';
+import { Direction } from '../../interface';
+import { Space, Button, ConfigProvider } from '../../../';
+
+function App() {
+    const [propDirection, setDirection] = React.useState<Direction>('ltr');
+    return (
+        <Space vertical align="start">
+            <Space>
+                <span>table direction = {propDirection}</span>
+                <span>ConfigProvider direction = rtl</span>
+                <Button onClick={() => setDirection('ltr')}>table prop ltr</Button>
+                <Button onClick={() => setDirection('rtl')}>table prop rtl</Button>
+                <Button onClick={() => setDirection()}>table prop undefined</Button>
+            </Space>
+            <div style={{ width: 800 }}>
+                <ConfigProvider direction='rtl'>
+                    <ColumnAlign direction={propDirection} />
+                </ConfigProvider>
+            </div>
+        </Space>
+    );
+}
+
+App.storyName = 'table direction rtl';
+App.parameters = {
+    chromatic: { disableSnapshot: false },
+};
+
+export default App;

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

@@ -1,2 +1,4 @@
 
 
-export { default as RTLAlignScrollBar } from './AlignScrollBar';
+export { default as RTLAlignScrollBar } from './AlignScrollBar';
+export { default as ColumnAlign } from './ColumnAlign';
+export { default as Direction } from './Direction';

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

@@ -70,7 +70,7 @@ export { default as VirtualizedDynamicData } from './VirtualizedDynamicData';
 export { default as MassiveColumns } from './MassiveColumns';
 export { default as MassiveColumns } from './MassiveColumns';
 export { default as ControlledPagination } from './ControlledPagination';
 export { default as ControlledPagination } from './ControlledPagination';
 export { default as FulldRenderDemo } from './FullRender';
 export { default as FulldRenderDemo } from './FullRender';
-export { RTLAlignScrollBar } from './RTL';
+export * from './RTL';
 export { default as JSXAsyncData } from './JSXAsyncData';
 export { default as JSXAsyncData } from './JSXAsyncData';
 export { default as ScrollBar } from './ScrollBar';
 export { default as ScrollBar } from './ScrollBar';
 export { default as TableSpan } from './TableSpan';
 export { default as TableSpan } from './TableSpan';

+ 86 - 0
packages/semi-ui/table/_story/v2/columnAlign.tsx

@@ -0,0 +1,86 @@
+import React from 'react';
+import { Table, Avatar, Space } from '../../../index';
+import { ColumnProps, Direction } from '../../../table/interface';
+
+export default function App(props: { direction?: Direction }) {
+    const { direction } = props;
+    const columns: ColumnProps[] = [
+        {
+            title: '标题 align left + fixed left',
+            dataIndex: 'name',
+            render: (text, record, index) => {
+                return (
+                    <Space spacing={12}>
+                        <Avatar
+                            size="small"
+                            shape="square"
+                            src={record.nameIconSrc}
+                        ></Avatar>
+                        {text}
+                    </Space>
+                );
+            },
+            align: 'left',
+            fixed: 'left',
+            width: 300
+        },
+        {
+            title: '大小 align center',
+            dataIndex: 'size',
+            align: 'center',
+            width: 200,
+        },
+        {
+            title: '所有者 align right',
+            dataIndex: 'owner',
+            render: (text, record, index) => {
+                return (
+                    <Space spacing={4}>
+                        <Avatar size="small" color={record.avatarBg}>
+                            {typeof text === 'string' && text.slice(0, 1)}
+                        </Avatar>
+                        {text}
+                    </Space>
+                );
+            },
+            align: 'right',
+        },
+        {
+            title: '更新日期 align default + fixed right',
+            dataIndex: 'updateTime',
+            width: 200,
+            fixed: 'right',
+        }
+    ];
+    const data = [
+        {
+            key: '1',
+            name: 'Semi Design 设计稿.fig',
+            nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
+            size: '2M',
+            owner: '姜鹏志',
+            updateTime: '2020-02-02 05:13',
+            avatarBg: 'grey',
+        },
+        {
+            key: '2',
+            name: 'Semi Design 分享演示文稿',
+            nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
+            size: '2M',
+            owner: '郝宣',
+            updateTime: '2020-01-17 05:31',
+            avatarBg: 'red',
+        },
+        {
+            key: '3',
+            name: '设计文档',
+            nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
+            size: '34KB',
+            owner: 'Zoey Edwards',
+            updateTime: '2020-01-26 11:01',
+            avatarBg: 'light-blue',
+        },
+    ];
+
+    return <Table direction={direction} bordered columns={columns} dataSource={data} scroll={{ y: 300, x: 1200 }} pagination={false} />;
+}

+ 7 - 2
packages/semi-ui/table/index.tsx

@@ -5,6 +5,7 @@ import ResizableTable from './ResizableTable';
 import Column from './Column';
 import Column from './Column';
 import { strings } from '@douyinfe/semi-foundation/table/constants';
 import { strings } from '@douyinfe/semi-foundation/table/constants';
 import { TableProps, Data } from './interface';
 import { TableProps, Data } from './interface';
+import ConfigContext, { ContextValue } from '../configProvider/context';
 
 
 class Table<RecordType extends Record<string, any> = Data> extends React.PureComponent<TableProps<RecordType>> {
 class Table<RecordType extends Record<string, any> = Data> extends React.PureComponent<TableProps<RecordType>> {
     static Column = Column;
     static Column = Column;
@@ -20,7 +21,10 @@ class Table<RecordType extends Record<string, any> = Data> extends React.PureCom
         hideExpandedColumn: true,
         hideExpandedColumn: true,
     };
     };
 
 
+    static contextType = ConfigContext;
+
     tableRef: React.RefObject<NormalTable<RecordType>>;
     tableRef: React.RefObject<NormalTable<RecordType>>;
+    context: ContextValue;
     constructor(props: TableProps) {
     constructor(props: TableProps) {
         super(props);
         super(props);
         this.tableRef = React.createRef();
         this.tableRef = React.createRef();
@@ -31,10 +35,11 @@ class Table<RecordType extends Record<string, any> = Data> extends React.PureCom
     render() {
     render() {
         // eslint-disable-next-line prefer-destructuring
         // eslint-disable-next-line prefer-destructuring
         const props = this.props;
         const props = this.props;
+        const direction = this.props.direction ?? this.context.direction;
         if (props.resizable) {
         if (props.resizable) {
-            return <ResizableTable {...props} ref={this.tableRef} />;
+            return <ResizableTable {...props} ref={this.tableRef} direction={direction} />;
         } else {
         } else {
-            return <NormalTable<RecordType> {...props} ref={this.tableRef} />;
+            return <NormalTable<RecordType> {...props} ref={this.tableRef} direction={direction} />;
         }
         }
     }
     }
 }
 }

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

@@ -70,7 +70,8 @@ export interface TableProps<RecordType extends Record<string, any> = any> extend
     onGroupedRow?: OnGroupedRow<RecordType>;
     onGroupedRow?: OnGroupedRow<RecordType>;
     onHeaderRow?: OnHeaderRow<RecordType>;
     onHeaderRow?: OnHeaderRow<RecordType>;
     onRow?: OnRow<RecordType>;
     onRow?: OnRow<RecordType>;
-    sticky?: Sticky
+    sticky?: Sticky;
+    direction?: Direction
 }
 }
 
 
 export interface ColumnProps<RecordType extends Record<string, any> = any> {
 export interface ColumnProps<RecordType extends Record<string, any> = any> {

+ 3 - 1
packages/semi-ui/table/table-context.ts

@@ -6,6 +6,7 @@ import {
     BaseRowKeyType,
     BaseRowKeyType,
     BaseHeadWidth,
     BaseHeadWidth,
 } from '@douyinfe/semi-foundation/table/foundation';
 } from '@douyinfe/semi-foundation/table/foundation';
+import type { ContextValue } from '../configProvider/context';
 
 
 export interface TableContextProps {
 export interface TableContextProps {
     children?: React.ReactNode;
     children?: React.ReactNode;
@@ -20,7 +21,8 @@ export interface TableContextProps {
     renderExpandIcon?: (record: Record<string, any>, isNested?: boolean, groupKey?: string | number) => React.ReactNode;
     renderExpandIcon?: (record: Record<string, any>, isNested?: boolean, groupKey?: string | number) => React.ReactNode;
     renderSelection?: (record?: Record<string, any>, isHeader?: boolean) => React.ReactNode;
     renderSelection?: (record?: Record<string, any>, isHeader?: boolean) => React.ReactNode;
     getVirtualizedListRef?: GetVirtualizedListRef;
     getVirtualizedListRef?: GetVirtualizedListRef;
-    setBodyHasScrollbar?: (bodyHasScrollBar: boolean) => void
+    setBodyHasScrollbar?: (bodyHasScrollBar: boolean) => void;
+    direction?: ContextValue['direction']
 }
 }
 
 
 const TableContext = React.createContext<TableContextProps>({
 const TableContext = React.createContext<TableContextProps>({