diff --git a/assets/index.less b/assets/index.less index 170e20b84..3115f72a3 100644 --- a/assets/index.less +++ b/assets/index.less @@ -25,6 +25,9 @@ box-sizing: border-box; position: relative; + &-rtl { + direction: rtl; + } // ================= Global ================= table { border-spacing: 0px; @@ -45,6 +48,10 @@ box-sizing: border-box; white-space: normal; word-break: break-word; + .@{tablePrefixCls}-rtl& { + border-left: @border; + border-right: 0; + } } // ================== Cell ================== @@ -58,6 +65,22 @@ border-right-color: transparent; } + .@{tablePrefixCls}-rtl & { + &-fix-right:last-child { + border-right-color: @border-color; + } + &-fix-left:last-child { + border-left-color: transparent; + } + } + + &-fix-left-first { + .@{tablePrefixCls}-rtl & { + box-shadow: 1px 0 0 @border-color; + } + } + + &-fix-left-first::after, &-fix-left-last::after { pointer-events: none; content: ''; @@ -70,9 +93,14 @@ transform: translateX(100%); } - &-fix-right-first { + &-fix-right-first, + &-fix-right-last { box-shadow: -1px 0 0 @border-color; + .@{tablePrefixCls}-rtl & { + box-shadow: none; + } + &::after { pointer-events: none; content: ''; @@ -92,8 +120,9 @@ text-overflow: ellipsis; // Fixed first or last should special process + &.@{tablePrefixCls}-cell-fix-left-first, &.@{tablePrefixCls}-cell-fix-left-last, - &.@{tablePrefixCls}-cell-fix-right-first { + &.@{tablePrefixCls}-cell-fix-right-first &.@{tablePrefixCls}-cell-fix-right-last { overflow: visible; .@{tablePrefixCls}-cell-content { @@ -106,13 +135,15 @@ } &-ping-left { + .@{tablePrefixCls}-cell-fix-left-first::after, .@{tablePrefixCls}-cell-fix-left-last::after { box-shadow: inset 10px 0 8px -8px green; } } &-ping-right { - .@{tablePrefixCls}-cell-fix-right-first::after { + .@{tablePrefixCls}-cell-fix-right-first::after, + .@{tablePrefixCls}-cell-fix-right-last::after { box-shadow: inset -10px 0 8px -8px green; } } @@ -142,6 +173,11 @@ left: -1px; width: 1px; background: @table-head-background-color; + + .@{tablePrefixCls}-rtl& { + right: -1px; + left: auto; + } } } diff --git a/examples/fixedColumnsAndHeaderRtl.tsx b/examples/fixedColumnsAndHeaderRtl.tsx new file mode 100644 index 000000000..16ceb0e74 --- /dev/null +++ b/examples/fixedColumnsAndHeaderRtl.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import Table from '../src'; +import '../assets/index.less'; +import { ColumnsType } from '../src/interface'; +import { useCheckbox } from './utils/useInput'; + +interface RecordType { + a: string; + b?: string; + c: string; + d: number; + key: string; +} + +const originData: RecordType[] = [ + { a: 'aaa', b: 'bbb', c: '内容内容内容内容内容', d: 3, key: '1' }, + { a: 'aaa', b: 'bbb', c: '内容内容内容内容内容', d: 3, key: '2' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '3' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '4' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '5' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '6' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '7' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '8' }, + { a: 'aaa', c: '内容内容内容内容内容', d: 2, key: '9' }, +]; + +const longTextData: RecordType[] = [...originData]; +longTextData[0] = { + ...longTextData[0], + a: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', +}; + +const useColumn = ( + fixLeft: boolean, + fixTitle: boolean, + fixRight: boolean, + ellipsis: boolean, + percentage: boolean, +) => { + const columns: ColumnsType = React.useMemo( + () => [ + { + title: 'title1', + dataIndex: 'a', + key: 'a', + width: percentage ? '10%' : 80, + fixed: fixLeft ? 'left' : null, + ellipsis, + }, + { title: 'title2', dataIndex: 'b', key: 'b', width: 80, fixed: fixLeft ? 'left' : null }, + { + title: 'title3', + fixed: fixLeft && fixTitle ? 'left' : null, + children: [ + { title: 'title4', dataIndex: 'c', key: 'd', width: 100 }, + { title: 'title5', dataIndex: 'c', key: 'e', width: 100 }, + ], + }, + { title: 'title6', dataIndex: 'c', key: 'f' }, + { title: 'title7', dataIndex: 'c', key: 'g' }, + { title: 'title8', dataIndex: 'c', key: 'h' }, + { title: 'title9', dataIndex: 'b', key: 'i' }, + { title: 'title10', dataIndex: 'b', key: 'j' }, + { title: 'title11', dataIndex: 'b', key: 'k', width: 100, fixed: fixRight ? 'right' : null }, + { title: 'title12', dataIndex: 'b', key: 'l', width: 80, fixed: fixRight ? 'right' : null }, + ], + [fixLeft, fixTitle, fixRight, ellipsis, percentage], + ); + + return columns; +}; + +const Demo = () => { + const [autoWidth, autoWidthProps] = useCheckbox(false); + const [isRtl, isRtlProps] = useCheckbox(true); + const [longText, longTextProps] = useCheckbox(false); + const [fixHeader, fixHeaderProps] = useCheckbox(true); + const [fixLeft, fixLeftProps] = useCheckbox(true); + const [fixRight, fixRightProps] = useCheckbox(true); + const [fixTitle3, fixTitle3Props] = useCheckbox(false); + const [ellipsis, ellipsisProps] = useCheckbox(false); + const [percentage, percentageProps] = useCheckbox(false); + const [empty, emptyProps] = useCheckbox(false); + const columns = useColumn(fixLeft, fixTitle3, fixRight, ellipsis, percentage); + + let mergedData: RecordType[]; + if (empty) { + mergedData = null; + } else if (longText) { + mergedData = longTextData; + } else { + mergedData = originData; + } + + return ( + +
+

Fixed columns and header in RTL direction

+ + + + + + + + + + + + + + columns={columns} + scroll={{ x: 1650, y: fixHeader ? 300 : null }} + data={mergedData} + style={{ width: autoWidth ? null : 800 }} + direction={isRtl ? 'rtl' : 'ltr'} + /> +
+
+ ); +}; + +export default Demo; diff --git a/src/Body/BodyRow.tsx b/src/Body/BodyRow.tsx index d7c9280a8..e8665e6eb 100644 --- a/src/Body/BodyRow.tsx +++ b/src/Body/BodyRow.tsx @@ -49,7 +49,7 @@ function BodyRow(props: BodyRowP cellComponent, childrenColumnName, } = props; - const { prefixCls } = React.useContext(TableContext); + const { prefixCls, direction } = React.useContext(TableContext); const { fixHeader, fixColumn, @@ -77,7 +77,7 @@ function BodyRow(props: BodyRowP // Move to Body to enhance performance const fixedInfoList = flattenColumns.map((column, colIndex) => - getCellFixedInfo(colIndex, colIndex, flattenColumns, stickyOffsets), + getCellFixedInfo(colIndex, colIndex, flattenColumns, stickyOffsets, direction), ); const rowSupportExpand = expandableType === 'row' && (!rowExpandable || rowExpandable(record)); diff --git a/src/Cell/index.tsx b/src/Cell/index.tsx index 4fb343ede..bef1d19ad 100644 --- a/src/Cell/index.tsx +++ b/src/Cell/index.tsx @@ -44,8 +44,10 @@ export interface CellProps { // Fixed fixLeft?: number | false; fixRight?: number | false; + firstFixLeft?: boolean; lastFixLeft?: boolean; firstFixRight?: boolean; + lastFixRight?: boolean; // Additional /** @private Used for `expandable` with nest tree */ @@ -67,8 +69,10 @@ function Cell( rowSpan, fixLeft, fixRight, + firstFixLeft, lastFixLeft, firstFixRight, + lastFixRight, appendNode, additionalProps = {}, ellipsis, @@ -134,6 +138,7 @@ function Cell( } if (isFixRight) { fixedStyle.position = 'sticky'; + fixedStyle.right = fixRight as number; } @@ -163,9 +168,11 @@ function Cell( className, { [`${cellPrefixCls}-fix-left`]: isFixLeft, + [`${cellPrefixCls}-fix-left-first`]: firstFixLeft, [`${cellPrefixCls}-fix-left-last`]: lastFixLeft, [`${cellPrefixCls}-fix-right`]: isFixRight, [`${cellPrefixCls}-fix-right-first`]: firstFixRight, + [`${cellPrefixCls}-fix-right-last`]: lastFixRight, [`${cellPrefixCls}-ellipsis`]: ellipsis, [`${cellPrefixCls}-with-append`]: appendNode, }, diff --git a/src/Header/FixedHeader.tsx b/src/Header/FixedHeader.tsx index c8fd2f557..31a91de98 100644 --- a/src/Header/FixedHeader.tsx +++ b/src/Header/FixedHeader.tsx @@ -7,6 +7,7 @@ import TableContext from '../context/TableContext'; export interface FixedHeaderProps extends HeaderProps { colWidths: number[]; columCount: number; + direction: 'ltr' | 'rtl'; } function FixedHeader({ @@ -15,6 +16,7 @@ function FixedHeader({ colWidths, columCount, stickyOffsets, + direction, ...props }: FixedHeaderProps) { const { prefixCls, scrollbarSize } = React.useContext(TableContext); @@ -40,11 +42,11 @@ function FixedHeader({ // Calculate the sticky offsets const headerStickyOffsets = React.useMemo(() => { - const { right } = stickyOffsets; - + const { right, left } = stickyOffsets; return { ...stickyOffsets, - right: [...right.map(width => width + scrollbarSize), 0], + left: direction === 'rtl' ? [...left.map(width => width + scrollbarSize), 0] : left, + right: direction === 'rtl' ? right : [...right.map(width => width + scrollbarSize), 0], }; }, [scrollbarSize, stickyOffsets]); diff --git a/src/Header/HeaderRow.tsx b/src/Header/HeaderRow.tsx index 15caf9d2a..74c3c39ab 100644 --- a/src/Header/HeaderRow.tsx +++ b/src/Header/HeaderRow.tsx @@ -30,7 +30,7 @@ function HeaderRow({ onHeaderRow, index, }: RowProps) { - const { prefixCls } = React.useContext(TableContext); + const { prefixCls, direction } = React.useContext(TableContext); let rowProps: React.HTMLAttributes; if (onHeaderRow) { @@ -48,6 +48,7 @@ function HeaderRow({ cell.colEnd, flattenColumns, stickyOffsets, + direction, ); let additionalProps: React.HTMLAttributes; diff --git a/src/Table.tsx b/src/Table.tsx index f579bfc89..d87dc23be 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -106,6 +106,8 @@ export interface TableProps extends LegacyExpandableProps< onHeaderRow?: GetComponentProps[]>; emptyText?: React.ReactNode | (() => React.ReactNode); + direction?: 'ltr' | 'rtl'; + // =================================== Internal =================================== /** * @private Internal usage, may remove by refactor. Should always use `columns` instead. @@ -142,6 +144,7 @@ function Table(props: TableProps(props: TableProps(props: TableProps colsWidths.get(columnKey)); - const stickyOffsets = useStickyOffsets(colWidths, flattenColumns.length); - + const stickyOffsets = useStickyOffsets(colWidths, flattenColumns.length, direction); const fixHeader = hasData && scroll && validateValue(scroll.y); const fixColumn = scroll && validateValue(scroll.x); @@ -505,7 +508,6 @@ function Table(props: TableProps { const colWidth = index === columns.length - 1 ? (width as number) - scrollbarSize : width; - if (typeof colWidth === 'number' && !Number.isNaN(colWidth)) { return colWidth; } @@ -553,7 +555,7 @@ function Table(props: TableProps - + )} @@ -587,6 +589,7 @@ function Table(props: TableProps(props: TableProps (null); diff --git a/src/hooks/useColumns.tsx b/src/hooks/useColumns.tsx index ff6460877..3d527c45b 100644 --- a/src/hooks/useColumns.tsx +++ b/src/hooks/useColumns.tsx @@ -82,6 +82,24 @@ function warningFixed(flattenColumns: { fixed?: FixedType }[]) { } } +function revertForRtl(columns: ColumnsType): ColumnsType { + return columns.map(column => { + const { fixed, ...restProps } = column; + + // Convert `fixed='left'` to `fixed='right'` instead + let parsedFixed = fixed; + if (fixed === 'left') { + parsedFixed = 'right'; + } else if (fixed === 'right') { + parsedFixed = 'left'; + } + return { + fixed: parsedFixed, + ...restProps, + }; + }); +} + /** * Parse `columns` & `children` into `columns`. */ @@ -97,6 +115,7 @@ function useColumns( expandIcon, rowExpandable, expandIconColumnIndex, + direction, }: { prefixCls?: string; columns?: ColumnsType; @@ -108,6 +127,7 @@ function useColumns( expandIcon?: RenderExpandIcon; rowExpandable?: (record: RecordType) => boolean; expandIconColumnIndex?: number; + direction?: 'ltr' | 'rtl'; }, transformColumns: (columns: ColumnsType) => ColumnsType, ): [ColumnsType, ColumnType[]] { @@ -151,7 +171,7 @@ function useColumns( return cloneColumns; } return baseColumns; - }, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon]); + }, [expandable, baseColumns, getRowKey, expandedKeys, expandIcon, direction]); const mergedColumns = React.useMemo(() => { let finalColumns = withExpandColumns; @@ -167,17 +187,19 @@ function useColumns( }, ]; } - return finalColumns; - }, [transformColumns, withExpandColumns]); - - const flattenColumns = React.useMemo(() => flatColumns(mergedColumns), [mergedColumns]); + }, [transformColumns, withExpandColumns, direction]); + const flattenColumns = React.useMemo(() => { + if (direction === 'rtl') { + return revertForRtl(flatColumns(mergedColumns)); + } + return flatColumns(mergedColumns); + }, [mergedColumns, direction]); // Only check out of production since it's waste for each render if (process.env.NODE_ENV !== 'production') { warningFixed(flattenColumns); } - return [mergedColumns, flattenColumns]; } diff --git a/src/hooks/useStickyOffsets.ts b/src/hooks/useStickyOffsets.ts index 8b94f431e..3497b8e7a 100644 --- a/src/hooks/useStickyOffsets.ts +++ b/src/hooks/useStickyOffsets.ts @@ -4,7 +4,7 @@ import { StickyOffsets } from '../interface'; /** * Get sticky column offset width */ -function useStickyOffsets(colWidths: number[], columCount: number) { +function useStickyOffsets(colWidths: number[], columCount: number, direction: 'ltr' | 'rtl') { const stickyOffsets: StickyOffsets = useMemo(() => { const leftOffsets: number[] = []; const rightOffsets: number[] = []; @@ -12,14 +12,25 @@ function useStickyOffsets(colWidths: number[], columCount: number) { let right = 0; for (let start = 0; start < columCount; start += 1) { - // Left offset - leftOffsets[start] = left; - left += colWidths[start] || 0; + if (direction === 'rtl') { + // Left offset + rightOffsets[start] = right; + right += colWidths[start] || 0; - // Right offset - const end = columCount - start - 1; - rightOffsets[end] = right; - right += colWidths[end] || 0; + // Right offset + const end = columCount - start - 1; + leftOffsets[end] = left; + left += colWidths[end] || 0; + } else { + // Left offset + leftOffsets[start] = left; + left += colWidths[start] || 0; + + // Right offset + const end = columCount - start - 1; + rightOffsets[end] = right; + right += colWidths[end] || 0; + } } return { diff --git a/src/utils/fixUtil.ts b/src/utils/fixUtil.ts index 1eedf7b51..6bd137d34 100644 --- a/src/utils/fixUtil.ts +++ b/src/utils/fixUtil.ts @@ -5,6 +5,10 @@ export interface FixedInfo { fixRight: number | false; lastFixLeft: boolean; firstFixRight: boolean; + + // For Rtl Direction + lastFixRight: boolean; + firstFixLeft: boolean; } export function getCellFixedInfo( @@ -12,6 +16,7 @@ export function getCellFixedInfo( colEnd: number, columns: { fixed?: FixedType }[], stickyOffsets: StickyOffsets, + direction: 'ltr' | 'rtl', ): FixedInfo { const startColumn = columns[colStart] || {}; const endColumn = columns[colEnd] || {}; @@ -27,12 +32,25 @@ export function getCellFixedInfo( let lastFixLeft: boolean = false; let firstFixRight: boolean = false; - if (fixLeft !== undefined) { - const nextColumn = columns[colEnd + 1]; + + let lastFixRight: boolean = false; + let firstFixLeft: boolean = false; + + const nextColumn = columns[colEnd + 1]; + const prevColumn = columns[colStart - 1]; + + if (direction === 'rtl') { + if (fixLeft !== undefined) { + const prevFixLeft = prevColumn && prevColumn.fixed === 'left'; + firstFixLeft = !prevFixLeft; + } else if (fixRight !== undefined) { + const nextFixRight = nextColumn && nextColumn.fixed === 'right'; + lastFixRight = !nextFixRight; + } + } else if (fixLeft !== undefined) { const nextFixLeft = nextColumn && nextColumn.fixed === 'left'; lastFixLeft = !nextFixLeft; } else if (fixRight !== undefined) { - const prevColumn = columns[colStart - 1]; const prevFixRight = prevColumn && prevColumn.fixed === 'right'; firstFixRight = !prevFixRight; } @@ -42,5 +60,7 @@ export function getCellFixedInfo( fixRight, lastFixLeft, firstFixRight, + lastFixRight, + firstFixLeft, }; } diff --git a/tests/FixedColumn.spec.js b/tests/FixedColumn.spec.js index db50e8576..d8655f0d9 100644 --- a/tests/FixedColumn.spec.js +++ b/tests/FixedColumn.spec.js @@ -156,4 +156,34 @@ describe('Table.FixedColumn', () => { expect(wrapper.find('tr th').find('.rc-table-cell-content')).toHaveLength(1); expect(wrapper.find('tr td').find('.rc-table-cell-content')).toHaveLength(data.length); }); + + it('fixed column renders correctly RTL', () => { + const wrapper = mount(); + expect(wrapper.render()).toMatchSnapshot(); + }); + it('has correct scroll classNames when table direction is RTL', () => { + const wrapper = mount(
); + + expect(wrapper.find('.rc-table').hasClass('rc-table-rtl')).toBeTruthy(); + + // Left should be right in RTL + expect( + wrapper + .find('.rc-table-row') + .first() + .find('.rc-table-cell') + .first() + .hasClass('rc-table-cell-fix-right'), + ).toBeTruthy(); + + // Right should be left in RTL + expect( + wrapper + .find('.rc-table-row') + .first() + .find('.rc-table-cell') + .last() + .hasClass('rc-table-cell-fix-left'), + ).toBeTruthy(); + }); }); diff --git a/tests/Table.spec.js b/tests/Table.spec.js index 96216c6d1..cd6ea9225 100644 --- a/tests/Table.spec.js +++ b/tests/Table.spec.js @@ -23,6 +23,17 @@ describe('Table.Basic', () => { expect(wrapper.render()).toMatchSnapshot(); }); + it('RTL', () => { + const wrapper = mount( + createTable({ + prefixCls: 'test-prefix', + className: 'test-class-name', + direction: 'rtl', + }), + ); + expect(wrapper.render()).toMatchSnapshot(); + }); + it('no columns', () => { const wrapper = mount(createTable({ columns: [] })); expect(wrapper.render()).toMatchSnapshot(); @@ -704,7 +715,7 @@ describe('Table.Basic', () => { - {'Invalidate Column'} + Invalidate Column
, ).render(), ).toMatchSnapshot(); diff --git a/tests/__snapshots__/FixedColumn.spec.js.snap b/tests/__snapshots__/FixedColumn.spec.js.snap index 7f3f23abc..5e4c53814 100644 --- a/tests/__snapshots__/FixedColumn.spec.js.snap +++ b/tests/__snapshots__/FixedColumn.spec.js.snap @@ -1,5 +1,583 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Table.FixedColumn fixed column renders correctly RTL 1`] = ` +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ title1 + + title2 + + title3 + + title4 + + title5 + + title6 + + title7 + + title8 + + title9 + + title10 + + title11 + + title12 +
+ 123 + + xxxxxxxx + + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx + + xxxxxxxx +
+ cdd + + edd12221 + + + edd12221 + + edd12221 + + edd12221 + + edd12221 + + edd12221 + + edd12221 + + edd12221 + + edd12221 + + edd12221 +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+ 133 + + + edd12221 + + + + + + + + + +
+
+
+
+`; + exports[`Table.FixedColumn renders correctly scrollX - with data 1`] = `
`; +exports[`Table.Basic renders correctly RTL 1`] = ` +
+
+
+ + + + + + + + + + + + + + + + + +
+ Name +
+ Lucy +
+ Jack +
+
+
+
+`; + exports[`Table.Basic renders correctly basic 1`] = `