diff --git a/assets/index.less b/assets/index.less index f75d255dd..8cfa66765 100644 --- a/assets/index.less +++ b/assets/index.less @@ -18,6 +18,23 @@ border-bottom: 0; } +.cellScrollBar(@bgColor: #fff) { + .@{tablePrefixCls}-cell-scrollbar::after { + position: absolute; + content: ''; + top: 0; + bottom: 0; + left: -1px; + width: 1px; + background: @bgColor; + + .@{tablePrefixCls}-rtl& { + right: -1px; + left: auto; + } + } +} + .@{tablePrefixCls} { font-size: @font-size-base; color: @text-color; @@ -165,20 +182,7 @@ text-align: center; } - .@{tablePrefixCls}-cell-scrollbar::after { - position: absolute; - content: ''; - top: 0; - bottom: 0; - left: -1px; - width: 1px; - background: @table-head-background-color; - - .@{tablePrefixCls}-rtl& { - right: -1px; - left: auto; - } - } + .cellScrollBar(@table-head-background-color); } &-header { @@ -205,19 +209,31 @@ border-radius: 5px 0 0 0; } - &-body { + &-body, + &-summary { .tableBorder(); border-top: 0; } - &-fixed-column &-body::after { - content: ''; - position: absolute; - right: 0; - top: 0; - bottom: 0; - border-right: @border; - z-index: 1; + &-fixed-column { + &-body, + &-summary { + &::after { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + border-right: @border; + z-index: 1; + } + } + } + + &-fixed-column &-summary { + &::-webkit-scrollbar { + display: none; + } } // ================= Expand ================= @@ -286,6 +302,7 @@ td { background: #fff; } + .cellScrollBar(); } &-sticky { &-header { diff --git a/docs/examples/fixedColumns.tsx b/docs/examples/fixedColumns.tsx index d65281c28..1042a1701 100644 --- a/docs/examples/fixedColumns.tsx +++ b/docs/examples/fixedColumns.tsx @@ -61,22 +61,26 @@ const Demo = () => ( b || c} - scroll={{ x: 1200 }} + scroll={{ x: 1200, y: 200 }} data={data} - summary={() => ( - <> - - - - Summary - - - Content - - Right - - - )} + summary={{ + render: () => ( + <> + + + + Summary + + + Content + + Right + + + ), + fixed: true, + position: 'bottom', + }} /> ); diff --git a/src/Footer/Cell.tsx b/src/Footer/Cell.tsx index a8d562599..1b45c00a4 100644 --- a/src/Footer/Cell.tsx +++ b/src/Footer/Cell.tsx @@ -20,9 +20,11 @@ export default function SummaryCell({ rowSpan, align, }: SummaryCellProps) { - const { prefixCls, fixedInfoList } = React.useContext(TableContext); + const { prefixCls, isSummaryFixed, fixedInfoList, summaryFixedInfoList } = React.useContext( + TableContext, + ); - const fixedInfo = fixedInfoList[index]; + const fixedInfo = isSummaryFixed ? summaryFixedInfoList[index] : fixedInfoList[index]; return ( ; +export default function FooterRow({ children, ...props }: FooterRowProps) { + const { + prefixCls, + scrollbarSize, + isSummaryFixed, + summaryFixedInfoList, + columnsWithScrollbar, + } = React.useContext(TableContext); + + const summaryListLength = summaryFixedInfoList.length; + const summaryColumn = columnsWithScrollbar[summaryListLength - 1]; + + let additionalProps: React.HTMLAttributes; + + if (isSummaryFixed && summaryColumn.onHeaderCell) { + additionalProps = summaryColumn.onHeaderCell(summaryColumn); + } + + return ( + + {children} + {isSummaryFixed && !!scrollbarSize && ( + + )} + + ); } diff --git a/src/Footer/index.tsx b/src/Footer/index.tsx index d6dd703ec..9be650eef 100644 --- a/src/Footer/index.tsx +++ b/src/Footer/index.tsx @@ -8,8 +8,12 @@ export interface FooterProps { } function Footer({ children }: FooterProps) { - const { prefixCls } = React.useContext(TableContext); - return {children}; + const { prefixCls, isSummaryShowTop } = React.useContext(TableContext); + return isSummaryShowTop ? ( + {children} + ) : ( + {children} + ); } export default Footer; diff --git a/src/Header/FixedHeader.tsx b/src/Header/FixedHeader.tsx index 40494e1c7..0466abcd0 100644 --- a/src/Header/FixedHeader.tsx +++ b/src/Header/FixedHeader.tsx @@ -7,6 +7,8 @@ import Header from './Header'; import ColGroup from '../ColGroup'; import type { ColumnsType, ColumnType } from '../interface'; import TableContext from '../context/TableContext'; +import useScrollBarColumns from '../hooks/useScrollBarColumns'; +import useCalcStickyOffsets from '../hooks/useCalcStickyOffsets'; function useColumnWidth(colWidths: readonly number[], columCount: number) { return useMemo(() => { @@ -56,8 +58,6 @@ const FixedHeader = React.forwardRef>( ) => { const { prefixCls, scrollbarSize, isSticky } = React.useContext(TableContext); - const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; - // Pass wheel to scroll event const scrollRef = React.useRef(null); @@ -88,38 +88,31 @@ const FixedHeader = React.forwardRef>( ); // Add scrollbar column - const lastColumn = flattenColumns[flattenColumns.length - 1]; - const ScrollBarColumn: ColumnType = { - fixed: lastColumn ? lastColumn.fixed : null, - onHeaderCell: () => ({ - className: `${prefixCls}-cell-scrollbar`, - }), - }; - - const columnsWithScrollbar = useMemo>( - () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), - [combinationScrollBarSize, columns], - ); - - const flattenColumnsWithScrollbar = useMemo( - () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), - [combinationScrollBarSize, flattenColumns], - ); + const { combinationScrollBarSize, columnsWithScrollbar } = useScrollBarColumns>({ + columns, + prefixCls, + scrollbarSize, + isSticky, + fixHeader, + }); + + const { columnsWithScrollbar: flattenColumnsWithScrollbar } = useScrollBarColumns< + ColumnType[] + >({ + columns: flattenColumns, + prefixCls, + scrollbarSize, + isSticky, + fixHeader, + }); // Calculate the sticky offsets - const headerStickyOffsets = useMemo(() => { - const { right, left } = stickyOffsets; - return { - ...stickyOffsets, - left: - direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, - right: - direction === 'rtl' - ? right - : [...right.map(width => width + combinationScrollBarSize), 0], - isSticky, - }; - }, [combinationScrollBarSize, stickyOffsets, isSticky]); + const headerStickyOffsets = useCalcStickyOffsets({ + stickyOffsets, + combinationScrollBarSize, + direction, + isSticky, + }); const mergedColumnWidth = useColumnWidth(colWidths, columCount); diff --git a/src/Table.tsx b/src/Table.tsx index 63ce3640d..82ad2d96a 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -54,6 +54,7 @@ import type { ColumnType, CustomizeScrollBody, TableSticky, + TableDirection, } from './interface'; import TableContext from './context/TableContext'; import BodyContext from './context/BodyContext'; @@ -71,6 +72,8 @@ import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil'; import { getCellFixedInfo } from './utils/fixUtil'; import StickyScrollBar from './stickyScrollBar'; import useSticky from './hooks/useSticky'; +import useScrollBarColumns from './hooks/useScrollBarColumns'; +import useCalcStickyOffsets from './hooks/useCalcStickyOffsets'; // Used for conditions cache const EMPTY_DATA = []; @@ -78,6 +81,10 @@ const EMPTY_DATA = []; // Used for customize scroll const EMPTY_SCROLL_TARGET = {}; +// Used for body content render +const BODY_CONTENT_TBODY = 'body'; +const BODY_CONTENT_TSUMMARY = 'summary'; + export const INTERNAL_HOOKS = 'rc-table-internal-hook'; interface MemoTableContentProps { @@ -87,6 +94,10 @@ interface MemoTableContentProps { props: any; } +type SummaryRender = (data: readonly RecordType[]) => React.ReactNode; + +type SummaryPosition = 'top' | 'bottom'; + const MemoTableContent = React.memo( ({ children }) => children as React.ReactElement, @@ -123,7 +134,13 @@ export interface TableProps extends LegacyExpandableProps< // Additional Part title?: PanelRender; footer?: PanelRender; - summary?: (data: readonly RecordType[]) => React.ReactNode; + summary?: + | SummaryRender + | { + render: SummaryRender; + fixed?: boolean; + position?: SummaryPosition; + }; // Customize id?: string; @@ -133,7 +150,7 @@ export interface TableProps extends LegacyExpandableProps< onHeaderRow?: GetComponentProps[]>; emptyText?: React.ReactNode | (() => React.ReactNode); - direction?: 'ltr' | 'rtl'; + direction?: TableDirection; // =================================== Internal =================================== /** @@ -199,6 +216,17 @@ function Table(props: TableProps = summary as any; + let summaryFixed: boolean = false; + let summaryPosition: SummaryPosition = 'bottom'; + + if (typeof summary === 'object' && summary !== null) { + summaryRender = summary.render; + summaryFixed = summary.fixed; + summaryPosition = summary.position; + } + // ===================== Effects ====================== const [scrollbarSize, setScrollbarSize] = React.useState(0); @@ -372,6 +400,7 @@ function Table(props: TableProps(); const scrollHeaderRef = React.useRef(); const scrollBodyRef = React.useRef(); + const scrollSummaryRef = React.useRef(); const [pingedLeft, setPingedLeft] = React.useState(false); const [pingedRight, setPingedRight] = React.useState(false); const [colsWidths, updateColsWidths] = useLayoutState(new Map()); @@ -396,6 +425,16 @@ function Table(props: TableProps[] + >({ + columns: flattenColumns, + prefixCls, + scrollbarSize, + isSticky, + fixHeader, + }); + if (fixHeader) { scrollYStyle = { overflowY: 'scroll', @@ -460,6 +499,7 @@ function Table(props: TableProps(props: TableProps ); - const bodyColGroup = ( - width)} columns={flattenColumns} /> + const flattenColWidths = flattenColumns.map(({ width }) => width); + + const BodyColGroup = () => ( + ); - const footerTable = summary && ; + const BodyColGroupWithScrollBar = () => ( + + ); + + const footerTable = summaryRender && ; const customizeScrollBody = getComponent(['body']) as CustomizeScrollBody; + const isTableFixed = fixHeader || isSticky; + + const isSummaryShowTop = isTableFixed && summaryPosition === 'top'; + const isSummaryFixed = isTableFixed && summaryFixed; + + const getBodyContent = (type: string, mergeSummary?: boolean) => { + const isBody = type === BODY_CONTENT_TBODY; + const ref = isBody ? scrollBodyRef : scrollSummaryRef; + const tableContent = isBody ? bodyTable : footerTable; + + return ( +
+ + {!isBody && summaryFixed ? : } + {tableContent} + {mergeSummary && type !== BODY_CONTENT_TSUMMARY && footerTable} + +
+ ); + }; + if ( process.env.NODE_ENV !== 'production' && typeof customizeScrollBody === 'function' && @@ -575,7 +658,7 @@ function Table(props: TableProps(props: TableProps - - {bodyColGroup} - {bodyTable} - {footerTable} - - + bodyContent = summaryFixed ? ( + <> + {footerTable && isSummaryShowTop && getBodyContent(BODY_CONTENT_TSUMMARY)} + {getBodyContent(BODY_CONTENT_TBODY)} + {footerTable && !isSummaryShowTop && getBodyContent(BODY_CONTENT_TSUMMARY)} + + ) : ( + getBodyContent(BODY_CONTENT_TBODY, true) ); } @@ -666,7 +736,7 @@ function Table(props: TableProps - {bodyColGroup} + {showHeader !== false &&
} {bodyTable} {footerTable} @@ -714,6 +784,13 @@ function Table(props: TableProps{fullTable}; } + const headerStickyOffsets = useCalcStickyOffsets({ + stickyOffsets, + combinationScrollBarSize, + direction, + isSticky, + }); + const TableContextValue = React.useMemo( () => ({ prefixCls, @@ -723,7 +800,13 @@ function Table(props: TableProps getCellFixedInfo(colIndex, colIndex, flattenColumns, stickyOffsets, direction), ), + summaryFixedInfoList: columnsWithScrollbar.map((_, colIndex) => + getCellFixedInfo(colIndex, colIndex, columnsWithScrollbar, headerStickyOffsets, direction), + ), + columnsWithScrollbar, isSticky, + isSummaryShowTop, + isSummaryFixed, }), [ prefixCls, @@ -731,9 +814,12 @@ function Table(props: TableProps[]; + isSticky: boolean; + + isSummaryShowTop: boolean; + + isSummaryFixed: boolean; } const TableContext = React.createContext(null); diff --git a/src/hooks/useCalcStickyOffsets.ts b/src/hooks/useCalcStickyOffsets.ts new file mode 100644 index 000000000..60762c270 --- /dev/null +++ b/src/hooks/useCalcStickyOffsets.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { StickyOffsets, TableDirection } from '../interface'; + +interface ICalcStickyOffsets { + stickyOffsets: StickyOffsets; + combinationScrollBarSize: number; + direction: TableDirection; + isSticky: boolean; +} + +export default function useCalcStickyOffsets({ + stickyOffsets, + combinationScrollBarSize, + direction, + isSticky, +}: ICalcStickyOffsets): StickyOffsets { + const headerStickyOffsets = React.useMemo(() => { + const { right, left } = stickyOffsets; + return { + ...stickyOffsets, + left: + direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, + right: + direction === 'rtl' ? right : [...right.map(width => width + combinationScrollBarSize), 0], + isSticky, + }; + }, [combinationScrollBarSize, stickyOffsets, isSticky]); + + return headerStickyOffsets; +} diff --git a/src/hooks/useScrollBarColumns.ts b/src/hooks/useScrollBarColumns.ts new file mode 100644 index 000000000..f24bd7822 --- /dev/null +++ b/src/hooks/useScrollBarColumns.ts @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { ColumnType } from '../interface'; + +interface IScrollBarColumns { + columns: any; + prefixCls: string; + scrollbarSize: number; + isSticky: boolean; + fixHeader: boolean; +} + +export default function useScrollBarColumns({ + columns, + prefixCls, + scrollbarSize, + isSticky, + fixHeader, +}: IScrollBarColumns): { + combinationScrollBarSize: number; + columnsWithScrollbar: T; +} { + const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; + + const lastColumn = columns[columns.length - 1]; + const ScrollBarColumn: ColumnType = { + fixed: lastColumn ? lastColumn.fixed : null, + onHeaderCell: () => ({ + className: `${prefixCls}-cell-scrollbar`, + }), + }; + + const columnsWithScrollbar = React.useMemo( + () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), + [combinationScrollBarSize, columns], + ); + + return { + combinationScrollBarSize, + columnsWithScrollbar, + }; +} diff --git a/src/interface.ts b/src/interface.ts index a338955c6..2bfd2fa0a 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -25,6 +25,8 @@ export type DefaultRecordType = Record; export type TableLayout = 'auto' | 'fixed'; +export type TableDirection = 'ltr' | 'rtl'; + // ==================== Row ===================== export type RowClassName = ( record: RecordType, diff --git a/tests/Table.spec.js b/tests/Table.spec.js index 2cc5ad977..47458c22a 100644 --- a/tests/Table.spec.js +++ b/tests/Table.spec.js @@ -201,6 +201,34 @@ describe('Table.Basic', () => { expect(wrapper.find('tfoot').render()).toMatchSnapshot(); }); + + it('support data type', () => { + const wrapper = mount( +
( + + + Light + + Bamboo + + ), + fixed: true, + position: 'top', + }} + scroll={{ x: 1200, y: 200 }} + />, + ); + + expect(wrapper.find('.rc-table-summary-content').render()).toMatchSnapshot(); + }); }); it('renders with id correctly', () => { diff --git a/tests/__snapshots__/Table.spec.js.snap b/tests/__snapshots__/Table.spec.js.snap index c19869220..991a09865 100644 --- a/tests/__snapshots__/Table.spec.js.snap +++ b/tests/__snapshots__/Table.spec.js.snap @@ -737,7 +737,7 @@ exports[`Table.Basic renders rowSpan correctly 1`] = ` exports[`Table.Basic summary support data type 1`] = ` + + + + + +`; + exports[`Table.Basic syntactic sugar 1`] = `
`; +exports[`Table.Basic summary support data type 2`] = ` +
+ Light + + Bamboo + +