diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index dd6c97db9d9..30dcc7caefc 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -1,5 +1,11 @@ # CHANGELOG +## NEXT_VERSION + +### Features + +- `n-data-table` 新增 `sticky-summary`、`summary-max-height` 属性,用于支持表格内容滚动时,总结栏固定不动,关闭 [#6585](https://github.com/tusen-ai/naive-ui/issues/6585)、[#6432](https://github.com/tusen-ai/naive-ui/issues/6432)、[#6170](https://github.com/tusen-ai/naive-ui/issues/6170)、[#2814](https://github.com/tusen-ai/naive-ui/issues/2814) + ## 2.41.0 `2025-01-05` diff --git a/src/data-table/demos/zhCN/index.demo-entry.md b/src/data-table/demos/zhCN/index.demo-entry.md index 73978e01842..90f7cf8ed8d 100644 --- a/src/data-table/demos/zhCN/index.demo-entry.md +++ b/src/data-table/demos/zhCN/index.demo-entry.md @@ -121,6 +121,8 @@ rtl-debug.vue | striped | `boolean` | `false` | 是否使用斑马线条纹 | | | summary | `DataTableCreateSummary` | `undefined` | 表格总结栏的数据,类型见 DataTableCreateSummary Type | | | summary-placement | `'top' \| 'bottom'` | `'bottom'` | 总结栏的位置 | 2.33.3 | +| sticky-summary | `boolean` | `false` | 总结栏是否不随表格纵向滚动 | NEXT_VERSION | +| summary-max-height | `number \| string` | `undefined` | 当 `sticky-summary` 为 `true` 时,总结栏最大高度,可以是 CSS 属性值 | NEXT_VERSION | | table-layout | `'auto' \| 'fixed'` | `'auto'` | 表格的 `table-layout` 样式属性,在设定 `ellipsis` 或 `max-height` 的情况下固定为 `'fixed'` | | | virtual-scroll | `boolean` | `false` | 是否开启虚拟滚动,应对大规模数据,开启前请设定好 `max-height`。当 `virtual-scroll` 为 `true` 时,`rowSpan` 将不生效 | | | virtual-scroll-header | `boolean` | `false` | 是否打开表头的虚拟滚动,如果横向列太多,可以考虑打开此属性,打开此属性会导致表头单元格跨行列的功能不可用,同时必须要配置 `header-height` | 2.40.0 | diff --git a/src/data-table/demos/zhCN/summary-debug.demo.vue b/src/data-table/demos/zhCN/summary-debug.demo.vue index ae8a5f5dd23..72c16faaeb9 100644 --- a/src/data-table/demos/zhCN/summary-debug.demo.vue +++ b/src/data-table/demos/zhCN/summary-debug.demo.vue @@ -3,8 +3,16 @@ diff --git a/src/data-table/src/DataTable.tsx b/src/data-table/src/DataTable.tsx index 1728b0a722c..bff2e9be3c4 100644 --- a/src/data-table/src/DataTable.tsx +++ b/src/data-table/src/DataTable.tsx @@ -278,6 +278,8 @@ export default defineComponent({ mergedTableLayoutRef, maxHeightRef: toRef(props, 'maxHeight'), minHeightRef: toRef(props, 'minHeight'), + summaryMaxHeightRef: toRef(props, 'summaryMaxHeight'), + stickySummaryRef: toRef(props, 'stickySummary'), flexHeightRef: toRef(props, 'flexHeight'), headerCheckboxDisabledRef, paginationBehaviorOnFilterRef: toRef(props, 'paginationBehaviorOnFilter'), diff --git a/src/data-table/src/TableParts/Body.tsx b/src/data-table/src/TableParts/Body.tsx index 2674c69913c..7e56cebb48e 100644 --- a/src/data-table/src/TableParts/Body.tsx +++ b/src/data-table/src/TableParts/Body.tsx @@ -178,6 +178,8 @@ export default defineComponent({ indentRef, rowPropsRef, maxHeightRef, + summaryMaxHeightRef, + stickySummaryRef, stripedRef, loadingRef, onLoadRef, @@ -198,6 +200,15 @@ export default defineComponent({ const NConfigProvider = inject(configProviderInjectionKey) const scrollbarInstRef = ref(null) const virtualListRef = ref(null) + const summaryScrollable = computed(() => { + const scrollable = summaryMaxHeightRef.value !== undefined + return ( + scrollable || (!scrollable && mergedTableLayoutRef.value === 'auto') + ) + }) + const summaryScrollElRef = ref(null) + const summaryScrollbarInstRef = ref(null) + const summaryVirtualListRef = ref(null) const emptyElRef = ref(null) const emptyRef = useMemo(() => paginatedDataRef.value.length === 0) // If header is not inside & empty is displayed, no table part would be @@ -341,6 +352,7 @@ export default defineComponent({ function handleVirtualListScroll(e: Event): void { handleTableBodyScroll(e) scrollbarInstRef.value?.sync() + setSummaryScrollLeft() } function handleVirtualListResize(e: ResizeObserverEntry): void { const { onResize } = props @@ -348,6 +360,53 @@ export default defineComponent({ onResize(e) scrollbarInstRef.value?.sync() } + function summaryVirtualListContainer(): HTMLElement | null { + const { value } = summaryVirtualListRef + return value?.listElRef || null + } + function summaryVirtualListContent(): HTMLElement | null { + const { value } = summaryVirtualListRef + return value?.itemsElRef || null + } + function handleSummaryVirtualListResize() { + summaryScrollbarInstRef.value?.sync() + } + function getSummaryScrollContainer() { + if (!virtualScrollRef.value) { + return summaryScrollable.value + ? summaryScrollbarInstRef.value?.containerRef + : summaryScrollElRef.value + } + else { + return summaryVirtualListRef.value?.listElRef + } + } + function setSummaryScrollLeft() { + const body = getScrollContainer() + if (!body) { + return + } + const summaryScrollContainer = getSummaryScrollContainer() + if (summaryScrollContainer) { + summaryScrollContainer.scrollLeft = body.scrollLeft + } + } + let lastSummaryScrollLeft = 0 + function handleSummaryScroll(e: Event): void { + const body = getScrollContainer() + const scrollLeft = (e.target as HTMLElement).scrollLeft + if (lastSummaryScrollLeft !== scrollLeft) { + if (body) { + body.scrollLeft = scrollLeft + } + lastSummaryScrollLeft = scrollLeft + } + else { + if (virtualScrollRef.value) { + summaryScrollbarInstRef.value?.sync() + } + } + } const exposedMethods: MainTableBodyRef = { getScrollContainer, scrollTo(arg0: any, arg1?: any) { @@ -450,6 +509,9 @@ export default defineComponent({ componentId, scrollbarInstRef, virtualListRef, + summaryScrollElRef, + summaryScrollbarInstRef, + summaryVirtualListRef, emptyElRef, summary: summaryRef, mergedClsPrefix: mergedClsPrefixRef, @@ -509,6 +571,9 @@ export default defineComponent({ indent: indentRef, rowProps: rowPropsRef, maxHeight: maxHeightRef, + summaryScrollable, + summaryMaxHeight: summaryMaxHeightRef, + stickySummary: stickySummaryRef, loadingKeySet: loadingKeySetRef, expandable: expandableRef, stickyExpandedRows: stickyExpandedRowsRef, @@ -517,10 +582,17 @@ export default defineComponent({ setHeaderScrollLeft, handleVirtualListScroll, handleVirtualListResize, + handleSummaryScroll, handleMouseleaveTable, virtualListContainer, virtualListContent, - handleTableBodyScroll, + summaryVirtualListContainer, + summaryVirtualListContent, + handleSummaryVirtualListResize, + handleTableBodyScroll(e: Event) { + handleTableBodyScroll(e) + setSummaryScrollLeft() + }, handleCheckboxUpdateChecked, handleRadioUpdateChecked, handleUpdateExpanded, @@ -530,11 +602,13 @@ export default defineComponent({ }, render() { const { - mergedTheme, scrollX, mergedClsPrefix, virtualScroll, maxHeight, + summaryScrollable, + summaryMaxHeight, + stickySummary, mergedTableLayout, flexHeight, loadingKeySet, @@ -557,593 +631,751 @@ export default defineComponent({ if (scrollX) contentStyle.width = '100%' - const tableNode = ( - - {{ - default: () => { - // coordinate to pass if there are cells that cross row & col - const cordToPass: Record = {} - // coordinate to related hover keys - const cordKey: Record> = {} - const { - cols, - paginatedDataAndInfo, - mergedTheme, - fixedColumnLeftMap, - fixedColumnRightMap, - currentPage, - rowClassName, - mergedSortState, - mergedExpandedRowKeySet, - stickyExpandedRows, - componentId, - childTriggerColIndex, - expandable, - rowProps, - handleMouseleaveTable, - renderExpand, - summary, - handleCheckboxUpdateChecked, - handleRadioUpdateChecked, - handleUpdateExpanded, - heightForRow, - minRowHeight, - virtualScrollX - } = this - const { length: colCount } = cols + const createTableNode = () => { + // coordinate to pass if there are cells that cross row & col + const cordToPass: Record = {} + // coordinate to related hover keys + const cordKey: Record> = {} + const { + cols, + paginatedDataAndInfo, + mergedTheme, + fixedColumnLeftMap, + fixedColumnRightMap, + currentPage, + rowClassName, + mergedSortState, + mergedExpandedRowKeySet, + stickyExpandedRows, + componentId, + childTriggerColIndex, + expandable, + rowProps, + handleMouseleaveTable, + renderExpand, + summary, + handleCheckboxUpdateChecked, + handleRadioUpdateChecked, + handleUpdateExpanded, + heightForRow, + minRowHeight, + virtualScrollX, + handleSummaryScroll + } = this + const { length: colCount } = cols - let mergedData: RowRenderInfo[] + let mergedData: RowRenderInfo[] - // if there is children in data, we should expand mergedData first + // if there is children in data, we should expand mergedData first - const { data: paginatedData, hasChildren } = paginatedDataAndInfo + const { data: paginatedData, hasChildren } = paginatedDataAndInfo - const mergedPaginationData = hasChildren - ? flatten(paginatedData, mergedExpandedRowKeySet) - : paginatedData + const mergedPaginationData = hasChildren + ? flatten(paginatedData, mergedExpandedRowKeySet) + : paginatedData - if (summary) { - const summaryRows = summary(this.rawPaginatedData) - if (Array.isArray(summaryRows)) { - const summaryRowData = summaryRows.map((row, i) => ({ - isSummaryRow: true as const, - key: `__n_summary__${i}`, - tmNode: { - rawNode: row, - disabled: true - }, - index: -1 - })) - mergedData - = this.summaryPlacement === 'top' - ? [...summaryRowData, ...mergedPaginationData] - : [...mergedPaginationData, ...summaryRowData] - } - else { - const summaryRowData = { - isSummaryRow: true as const, - key: '__n_summary__', - tmNode: { - rawNode: summaryRows, - disabled: true - }, - index: -1 - } - mergedData - = this.summaryPlacement === 'top' - ? [summaryRowData, ...mergedPaginationData] - : [...mergedPaginationData, summaryRowData] - } - } - else { - mergedData = mergedPaginationData + const summaryRows = summary ? summary(this.rawPaginatedData) : [] + const summaryRowData = Array.isArray(summaryRows) + ? summaryRows.map((row, i) => ({ + isSummaryRow: true as const, + key: `__n_summary__${i}`, + tmNode: { + rawNode: row, + disabled: true + }, + index: -1 + })) + : [ + { + isSummaryRow: true as const, + key: '__n_summary__', + tmNode: { + rawNode: summaryRows, + disabled: true + }, + index: -1 } + ] - const indentStyle = hasChildren - ? { width: pxfy(this.indent) } - : undefined + if ((!stickySummary || this.showHeader) && summaryRowData.length > 0) { + mergedData + = this.summaryPlacement === 'top' + ? [...summaryRowData, ...mergedPaginationData] + : [...mergedPaginationData, ...summaryRowData] + } + else { + mergedData = mergedPaginationData + } - // Tile the data of the expanded row - const displayedData: RowRenderInfo[] = [] - mergedData.forEach((rowInfo) => { - if ( - renderExpand - && mergedExpandedRowKeySet.has(rowInfo.key) - && (!expandable || expandable(rowInfo.tmNode.rawNode)) - ) { - displayedData.push(rowInfo, { - isExpandedRow: true, - key: `${rowInfo.key}-expand`, // solve key repeat of the expanded row - tmNode: rowInfo.tmNode as TmNode, - index: rowInfo.index - }) - } - else { - displayedData.push(rowInfo) - } - }) + const indentStyle = hasChildren ? { width: pxfy(this.indent) } : undefined + + // Tile the data of the expanded row + const displayedData: RowRenderInfo[] = [] + mergedData.forEach((rowInfo) => { + if ( + renderExpand + && mergedExpandedRowKeySet.has(rowInfo.key) + && (!expandable || expandable(rowInfo.tmNode.rawNode)) + ) { + displayedData.push(rowInfo, { + isExpandedRow: true, + key: `${rowInfo.key}-expand`, // solve key repeat of the expanded row + tmNode: rowInfo.tmNode as TmNode, + index: rowInfo.index + }) + } + else { + displayedData.push(rowInfo) + } + }) - const { length: rowCount } = displayedData + const { length: rowCount } = displayedData - const rowIndexToKey: Record = {} - paginatedData.forEach(({ tmNode }, rowIndex) => { - rowIndexToKey[rowIndex] = tmNode.key - }) + const rowIndexToKey: Record = {} + paginatedData.forEach(({ tmNode }, rowIndex) => { + rowIndexToKey[rowIndex] = tmNode.key + }) - const bodyWidth = stickyExpandedRows ? this.bodyWidth : null - const bodyWidthPx - = bodyWidth === null ? undefined : `${bodyWidth}px` + const bodyWidth = stickyExpandedRows ? this.bodyWidth : null + const bodyWidthPx = bodyWidth === null ? undefined : `${bodyWidth}px` - const CellComponent = (this.virtualScrollX ? 'div' : 'td') as 'td' - let leftFixedColsCount = 0 - let rightFixedColsCount = 0 - if (virtualScrollX) { - cols.forEach((col) => { - if (col.column.fixed === 'left') { - leftFixedColsCount++ - } - else if (col.column.fixed === 'right') { - rightFixedColsCount++ - } - }) - } + const CellComponent = (this.virtualScrollX ? 'div' : 'td') as 'td' + let leftFixedColsCount = 0 + let rightFixedColsCount = 0 + if (virtualScrollX) { + cols.forEach((col) => { + if (col.column.fixed === 'left') { + leftFixedColsCount++ + } + else if (col.column.fixed === 'right') { + rightFixedColsCount++ + } + }) + } - const renderRow = ({ - // Normal - rowInfo, - displayedRowIndex, - isVirtual, - // Virtual X - isVirtualX, - startColIndex, - endColIndex, - getLeft - }: { - rowInfo: RowRenderInfo - displayedRowIndex: number - isVirtual: boolean - // for horizontal virtual list - isVirtualX: boolean - startColIndex: number - endColIndex: number - getLeft: (index: number) => number - }): VNode => { - const { index: actualRowIndex } = rowInfo - if ('isExpandedRow' in rowInfo) { - const { - tmNode: { key, rawNode } - } = rowInfo - return ( - number + }): VNode => { + const { index: actualRowIndex } = rowInfo + if ('isExpandedRow' in rowInfo) { + const { + tmNode: { key, rawNode } + } = rowInfo + return ( + + + {stickyExpandedRows ? ( +
- - {stickyExpandedRows ? ( -
- {renderExpand!(rawNode, actualRowIndex)} -
- ) : ( - renderExpand!(rawNode, actualRowIndex) - )} - - - ) + {renderExpand!(rawNode, actualRowIndex)} +
+ ) : ( + renderExpand!(rawNode, actualRowIndex) + )} + + + ) + } + const isSummary = 'isSummaryRow' in rowInfo + const striped = !isSummary && rowInfo.striped + const { tmNode, key: rowKey } = rowInfo + const { rawNode: rowData } = tmNode + const expanded = mergedExpandedRowKeySet.has(rowKey) + const props = rowProps ? rowProps(rowData, actualRowIndex) : undefined + const mergedRowClassName + = typeof rowClassName === 'string' + ? rowClassName + : createRowClassName(rowData, actualRowIndex, rowClassName) + const iteratedCols = isVirtualX + ? cols.filter((col, index) => { + if (startColIndex <= index && index <= endColIndex) + return true + if (col.column.fixed) { + return true } - const isSummary = 'isSummaryRow' in rowInfo - const striped = !isSummary && rowInfo.striped - const { tmNode, key: rowKey } = rowInfo - const { rawNode: rowData } = tmNode - const expanded = mergedExpandedRowKeySet.has(rowKey) - const props = rowProps - ? rowProps(rowData, actualRowIndex) - : undefined - const mergedRowClassName - = typeof rowClassName === 'string' - ? rowClassName - : createRowClassName(rowData, actualRowIndex, rowClassName) - const iteratedCols = isVirtualX - ? cols.filter((col, index) => { - if (startColIndex <= index && index <= endColIndex) - return true - if (col.column.fixed) { - return true - } - return false - }) - : cols - const virtualXRowHeight = isVirtualX - ? pxfy(heightForRow?.(rowData, actualRowIndex) || minRowHeight) - : undefined - const cells = iteratedCols.map((col) => { - const colIndex = col.index - if (displayedRowIndex in cordToPass) { - const cordOfRowToPass = cordToPass[displayedRowIndex] - const indexInCordOfRowToPass - = cordOfRowToPass.indexOf(colIndex) - if (~indexInCordOfRowToPass) { - cordOfRowToPass.splice(indexInCordOfRowToPass, 1) - return null - } - } - // TODO: Simplify row calculation - const { column } = col - const colKey = getColKey(col) - const { rowSpan, colSpan } = column - const mergedColSpan = isSummary - ? rowInfo.tmNode.rawNode[colKey]?.colSpan || 1 // optional for #1276 - : colSpan - ? colSpan(rowData, actualRowIndex) - : 1 - const mergedRowSpan = isSummary - ? rowInfo.tmNode.rawNode[colKey]?.rowSpan || 1 // optional for #1276 - : rowSpan - ? rowSpan(rowData, actualRowIndex) - : 1 - const isLastCol = colIndex + mergedColSpan === colCount - const isLastRow = displayedRowIndex + mergedRowSpan === rowCount - const isCrossRowTd = mergedRowSpan > 1 - if (isCrossRowTd) { - cordKey[displayedRowIndex] = { - [colIndex]: [] - } + return false + }) + : cols + const virtualXRowHeight = isVirtualX + ? pxfy(heightForRow?.(rowData, actualRowIndex) || minRowHeight) + : undefined + const cells = iteratedCols.map((col) => { + const colIndex = col.index + if (displayedRowIndex in cordToPass) { + const cordOfRowToPass = cordToPass[displayedRowIndex] + const indexInCordOfRowToPass = cordOfRowToPass.indexOf(colIndex) + if (~indexInCordOfRowToPass) { + cordOfRowToPass.splice(indexInCordOfRowToPass, 1) + return null + } + } + // TODO: Simplify row calculation + const { column } = col + const colKey = getColKey(col) + const { rowSpan, colSpan } = column + const mergedColSpan = isSummary + ? rowInfo.tmNode.rawNode[colKey]?.colSpan || 1 // optional for #1276 + : colSpan + ? colSpan(rowData, actualRowIndex) + : 1 + const mergedRowSpan = isSummary + ? rowInfo.tmNode.rawNode[colKey]?.rowSpan || 1 // optional for #1276 + : rowSpan + ? rowSpan(rowData, actualRowIndex) + : 1 + const isLastCol = colIndex + mergedColSpan === colCount + const isLastRow = displayedRowIndex + mergedRowSpan === rowCount + const isCrossRowTd = mergedRowSpan > 1 + if (isCrossRowTd) { + cordKey[displayedRowIndex] = { + [colIndex]: [] + } + } + if (mergedColSpan > 1 || isCrossRowTd) { + for ( + let i = displayedRowIndex; + i < displayedRowIndex + mergedRowSpan; + ++i + ) { + if (isCrossRowTd) { + cordKey[displayedRowIndex][colIndex].push(rowIndexToKey[i]) + } + for (let j = colIndex; j < colIndex + mergedColSpan; ++j) { + if (i === displayedRowIndex && j === colIndex) { + continue } - if (mergedColSpan > 1 || isCrossRowTd) { - for ( - let i = displayedRowIndex; - i < displayedRowIndex + mergedRowSpan; - ++i - ) { - if (isCrossRowTd) { - cordKey[displayedRowIndex][colIndex].push( - rowIndexToKey[i] - ) - } - for (let j = colIndex; j < colIndex + mergedColSpan; ++j) { - if (i === displayedRowIndex && j === colIndex) { - continue - } - if (!(i in cordToPass)) { - cordToPass[i] = [j] - } - else { - cordToPass[i].push(j) - } - } - } + if (!(i in cordToPass)) { + cordToPass[i] = [j] } - const hoverKey = isCrossRowTd ? this.hoverKey : null - const { cellProps } = column - const resolvedCellProps = cellProps?.(rowData, actualRowIndex) - const indentOffsetStyle = { - '--indent-offset': '' as string | number + else { + cordToPass[i].push(j) } - const FinalCellComponent = column.fixed ? 'td' : CellComponent - return ( - - {hasChildren && colIndex === childTriggerColIndex - ? [ - repeat( - (indentOffsetStyle['--indent-offset'] = isSummary - ? 0 - : rowInfo.tmNode.level), -
- ), - isSummary || rowInfo.tmNode.isLeaf ? ( -
- ) : ( - { - handleUpdateExpanded(rowKey, rowInfo.tmNode) - }} - /> - ) - ] - : null} - {column.type === 'selection' ? ( - !isSummary ? ( - column.multiple === false ? ( - { - handleRadioUpdateChecked(rowInfo.tmNode) - }} - /> - ) : ( - { - handleCheckboxUpdateChecked( - rowInfo.tmNode, - checked, - e.shiftKey - ) - }} - /> - ) - ) : null - ) : column.type === 'expand' ? ( - !isSummary ? ( - !column.expandable || column.expandable?.(rowData) ? ( - { - handleUpdateExpanded(rowKey, null) - }} - /> - ) : null - ) : null + } + } + } + const hoverKey = isCrossRowTd ? this.hoverKey : null + const { cellProps } = column + const resolvedCellProps = cellProps?.(rowData, actualRowIndex) + const indentOffsetStyle = { + '--indent-offset': '' as string | number + } + const FinalCellComponent = column.fixed ? 'td' : CellComponent + return ( + + {hasChildren && colIndex === childTriggerColIndex + ? [ + repeat( + (indentOffsetStyle['--indent-offset'] = isSummary + ? 0 + : rowInfo.tmNode.level), +
+ ), + isSummary || rowInfo.tmNode.isLeaf ? ( +
) : ( - { + handleUpdateExpanded(rowKey, rowInfo.tmNode) + }} /> - )} - - ) - }) + ) + ] + : null} + {column.type === 'selection' ? ( + !isSummary ? ( + column.multiple === false ? ( + { + handleRadioUpdateChecked(rowInfo.tmNode) + }} + /> + ) : ( + { + handleCheckboxUpdateChecked( + rowInfo.tmNode, + checked, + e.shiftKey + ) + }} + /> + ) + ) : null + ) : column.type === 'expand' ? ( + !isSummary ? ( + !column.expandable || column.expandable?.(rowData) ? ( + { + handleUpdateExpanded(rowKey, null) + }} + /> + ) : null + ) : null + ) : ( + + )} + + ) + }) - if (isVirtualX) { - if (leftFixedColsCount && rightFixedColsCount) { - cells.splice( - leftFixedColsCount, - 0, - + ) + } + } + + const row = ( + { + this.hoverKey = rowKey + props?.onMouseenter?.(e) + }} + key={rowKey} + class={[ + `${mergedClsPrefix}-data-table-tr`, + isSummary && `${mergedClsPrefix}-data-table-tr--summary`, + striped && `${mergedClsPrefix}-data-table-tr--striped`, + expanded && `${mergedClsPrefix}-data-table-tr--expanded`, + mergedRowClassName, + props?.class + ]} + style={[props?.style, isVirtualX && { height: virtualXRowHeight }]} + > + {cells} + + ) + return row + } + + const createSummaryNode = () => { + if (summaryRowData.length === 0) { + return null + } + const summaryRowCount + = this.summaryPlacement === 'top' ? -1 : summaryRowData.length + const summaryStyle = { + maxHeight: formatLength(summaryMaxHeight) + } + + const summaryTableNode = ( + + + {cols.map(col => ( + + ))} + + + {summaryRowData.map((rowInfo, displayedRowIndex) => { + return renderRow({ + rowInfo, + displayedRowIndex, + isVirtual: false, + isVirtualX: false, + startColIndex: -1, + endColIndex: -1, + rowCount: summaryRowCount, + getLeft(_index) { + return -1 + } + }) + })} + +
+ ) + + if (!summaryScrollable && !virtualScroll) { + return ( +
+ {summaryTableNode} +
+ ) + } + + return ( + + {{ + default: () => { + if (!virtualScroll) { + return summaryTableNode + } + else { + return ( + { + return renderRow({ + displayedRowIndex: itemIndex, + isVirtual: true, + isVirtualX: true, + rowInfo: item as RowRenderInfo, + startColIndex, + endColIndex, + rowCount: summaryRowCount, + getLeft + }) + } + : undefined } - style={{ - pointerEvents: 'none', - visibility: 'hidden', - height: 0 + > + {{ + default: ({ + item, + index, + renderedItemWithCols + }: { + item: RowRenderInfo + index: number + renderedItemWithCols: VNodeChild + }) => { + if (renderedItemWithCols) + return renderedItemWithCols + return renderRow({ + rowInfo: item, + displayedRowIndex: index, + isVirtual: true, + isVirtualX: false, + startColIndex: 0, + endColIndex: 0, + rowCount: summaryRowCount, + getLeft(_index) { + return 0 + } + }) + } }} - /> + ) } } + }} + + ) + } - const row = ( - { - this.hoverKey = rowKey - props?.onMouseenter?.(e) - }} - key={rowKey} - class={[ - `${mergedClsPrefix}-data-table-tr`, - isSummary && `${mergedClsPrefix}-data-table-tr--summary`, - striped && `${mergedClsPrefix}-data-table-tr--striped`, - expanded && `${mergedClsPrefix}-data-table-tr--expanded`, - mergedRowClassName, - props?.class - ]} - style={[ - props?.style, - isVirtualX && { height: virtualXRowHeight } - ]} - > - {cells} - - ) - return row - } - - if (!virtualScroll) { - return ( - - - {cols.map(col => ( - - ))} - - {this.showHeader ? : null} - {!this.empty ? ( - + {stickySummary && !this.showHeader && this.summaryPlacement === 'top' + ? createSummaryNode() + : null} + + {{ + default: () => { + if (!virtualScroll) { + return ( +
- {displayedData.map((rowInfo, displayedRowIndex) => { - return renderRow({ - rowInfo, - displayedRowIndex, - isVirtual: false, - isVirtualX: false, - startColIndex: -1, - endColIndex: -1, - getLeft(_index) { - return -1 - } - }) - })} - - ) : null} -
- ) - } - else { - return ( - + {cols.map(col => ( + + ))} + + {this.showHeader ? ( + + ) : null} + {!this.empty ? ( + + {displayedData.map((rowInfo, displayedRowIndex) => { + return renderRow({ + rowInfo, + displayedRowIndex, + isVirtual: false, + isVirtualX: false, + startColIndex: -1, + endColIndex: -1, + rowCount, + getLeft(_index) { + return -1 + } + }) + })} + + ) : null} + + ) + } + else { + return ( + { + return renderRow({ + displayedRowIndex: itemIndex, + isVirtual: true, + isVirtualX: true, + rowInfo: item as RowRenderInfo, + startColIndex, + endColIndex, + rowCount, + getLeft + }) + } + : undefined + } + > + {{ + default: ({ item, - startColIndex, - endColIndex, - getLeft + index, + renderedItemWithCols + }: { + item: RowRenderInfo + index: number + renderedItemWithCols: VNodeChild }) => { + if (renderedItemWithCols) + return renderedItemWithCols return renderRow({ - displayedRowIndex: itemIndex, + rowInfo: item, + displayedRowIndex: index, isVirtual: true, - isVirtualX: true, - rowInfo: item as RowRenderInfo, - startColIndex, - endColIndex, - getLeft + isVirtualX: false, + startColIndex: 0, + endColIndex: 0, + rowCount, + getLeft(_index) { + return 0 + } }) } - : undefined - } - > - {{ - default: ({ - item, - index, - renderedItemWithCols - }: { - item: RowRenderInfo - index: number - renderedItemWithCols: VNodeChild - }) => { - if (renderedItemWithCols) - return renderedItemWithCols - return renderRow({ - rowInfo: item, - displayedRowIndex: index, - isVirtual: true, - isVirtualX: false, - startColIndex: 0, - endColIndex: 0, - getLeft(_index) { - return 0 - } - }) - } - }} - - ) - } - } - }} - - ) + }} + + ) + } + } + }} + + {stickySummary && !this.showHeader && this.summaryPlacement !== 'top' + ? createSummaryNode() + : null} + + ) + } if (this.empty) { const createEmptyNode = (): VNode => ( @@ -1166,7 +1398,7 @@ export default defineComponent({ if (this.shouldDisplaySomeTablePart) { return ( <> - {tableNode} + {createTableNode()} {createEmptyNode()} ) @@ -1179,6 +1411,6 @@ export default defineComponent({ ) } } - return tableNode + return createTableNode() } }) diff --git a/src/data-table/src/interface.ts b/src/data-table/src/interface.ts index b965367cd76..5105cffd546 100644 --- a/src/data-table/src/interface.ts +++ b/src/data-table/src/interface.ts @@ -43,6 +43,8 @@ export const dataTableProps = { }, minHeight: [Number, String] as PropType, maxHeight: [Number, String] as PropType, + summaryMaxHeight: [Number, String] as PropType, + stickySummary: Boolean, // Use any type as row data to make prop data acceptable columns: { type: Array as PropType>, @@ -413,6 +415,8 @@ export interface DataTableInjection { mergedTableLayoutRef: Ref<'auto' | 'fixed'> maxHeightRef: Ref minHeightRef: Ref + summaryMaxHeightRef: Ref + stickySummaryRef: Ref rowPropsRef: Ref flexHeightRef: Ref headerCheckboxDisabledRef: Ref diff --git a/src/data-table/src/styles/index.cssr.ts b/src/data-table/src/styles/index.cssr.ts index 02abe931b98..f03171aef6c 100644 --- a/src/data-table/src/styles/index.cssr.ts +++ b/src/data-table/src/styles/index.cssr.ts @@ -86,7 +86,10 @@ export default c([ flex-grow: 1; `, [ c('>', [ - cB('data-table-base-table-body', 'flex-basis: 0;', [ + cB('data-table-base-table-body', ` + flex-basis: 0; + flex-grow: 1; + `, [ // last-child means there is no empty icon // body is a scrollbar, we need to override height 100% c('&:last-child', 'flex-grow: 1;') @@ -519,6 +522,19 @@ export default c([ height: 0; `) ]), + cB('data-table-base-table-summary', ` + z-index: 3; + overflow: scroll; + flex-shrink: 0; + transition: border-color .3s var(--n-bezier); + scrollbar-width: none; + `, [ + c('&::-webkit-scrollbar, &::-webkit-scrollbar-track-piece, &::-webkit-scrollbar-thumb', ` + display: none; + width: 0; + height: 0; + `) + ]), cB('data-table-check-extra', ` transition: color .3s var(--n-bezier); color: var(--n-th-icon-color);