diff --git a/packages/pxweb2-ui/src/lib/components/Table/Table.spec.tsx b/packages/pxweb2-ui/src/lib/components/Table/Table.spec.tsx index 9aa13c68c..5503f9f02 100644 --- a/packages/pxweb2-ui/src/lib/components/Table/Table.spec.tsx +++ b/packages/pxweb2-ui/src/lib/components/Table/Table.spec.tsx @@ -1,5 +1,63 @@ +import React from 'react'; import { render } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; + +type VirtualRowMock = { + className?: string; + cells: React.ReactNode; +}; + +type TableVirtuosoMockProps = { + data?: VirtualRowMock[]; + fixedHeaderContent?: () => React.ReactNode; + itemContent: (index: number, item: VirtualRowMock) => React.ReactNode; +}; + +function getRowKey(item: VirtualRowMock, index: number): string { + if (Array.isArray(item.cells)) { + const elementKeys = item.cells + .map((cell) => { + if (typeof cell !== 'object' || cell === null) { + return ''; + } + if ('key' in cell) { + return String((cell as React.ReactElement).key); + } + return ''; + }) + .join('-'); + if (elementKeys.length > 0) { + return elementKeys; + } + } + + if (React.isValidElement(item.cells) && item.cells.key) { + return String(item.cells.key); + } + + if (item.className && item.className.length > 0) { + return item.className + '-' + String(index); + } + + return 'row-' + String(index); +} + +vi.mock('react-virtuoso', () => ({ + TableVirtuoso: ({ + data = [], + fixedHeaderContent, + itemContent, + }: TableVirtuosoMockProps) => ( + + {fixedHeaderContent?.()} + + {data.map((item, index) => ( + {itemContent(index, item)} + ))} + +
+ ), +})); import Table from './Table'; import { pxTable } from './testData'; diff --git a/packages/pxweb2-ui/src/lib/components/Table/Table.tsx b/packages/pxweb2-ui/src/lib/components/Table/Table.tsx index 37a77140c..60605c53a 100644 --- a/packages/pxweb2-ui/src/lib/components/Table/Table.tsx +++ b/packages/pxweb2-ui/src/lib/components/Table/Table.tsx @@ -1,5 +1,6 @@ -import { memo, useMemo } from 'react'; +import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'; import cl from 'clsx'; +import { TableComponents, TableVirtuoso } from 'react-virtuoso'; import classes from './Table.module.scss'; import { PxTable } from '../../shared-types/pxTable'; @@ -56,13 +57,62 @@ interface CreateRowMobileParams { */ type DataCellCodes = DataCellMeta[]; +type VirtualTableRow = { + className?: string; + cells: React.ReactNode; +}; + +type VirtualTableContext = { + tableClassName: string; + tableLabel: string; +}; + +const tableVirtuosoComponents: TableComponents< + VirtualTableRow, + VirtualTableContext +> = { + Table: ({ children, style, context }) => ( + + {children} +
+ ), + TableRow: ({ item, ...props }) => ( + + ), +}; + export const Table = memo(function Table({ pxtable, isMobile, className = '', }: TableProps) { + const tableHostRef = useRef(null); + const [customScrollParent, setCustomScrollParent] = + useState(null); + const cssClasses = className.length > 0 ? ' ' + className : ''; + useLayoutEffect(() => { + let parent = tableHostRef.current?.parentElement ?? null; + while (parent) { + const style = window.getComputedStyle(parent); + const hasScrollableOverflow = /(auto|scroll|overlay)/.test( + style.overflowY, + ); + if (hasScrollableOverflow && parent.scrollHeight > parent.clientHeight) { + setCustomScrollParent(parent); + return; + } + parent = parent.parentElement; + } + + setCustomScrollParent(null); + }, [pxtable, isMobile]); + const tableMeta: columnRowMeta = calculateRowAndColumnMeta(pxtable); const tableColumnSize: number = tableMeta.columns - tableMeta.columnOffset; @@ -115,34 +165,57 @@ export const Table = memo(function Table({ headingDataCellCodes[i] = dataCellCodes; } + const headingRows = useMemo( + () => createHeading(pxtable, tableMeta, headingDataCellCodes), + [pxtable, tableMeta, headingDataCellCodes], + ); + + const tableRows = useMemo( + () => + createRows( + pxtable, + tableMeta, + headingDataCellCodes, + isMobile, + contentVarIndex, + contentsVariableDecimals, + ), + [ + pxtable, + tableMeta, + headingDataCellCodes, + isMobile, + contentVarIndex, + contentsVariableDecimals, + ], + ); + + const rowData = useMemo( + () => + tableRows.map((row) => ({ + className: row.props.className, + cells: row.props.children, + })), + [tableRows], + ); + return ( - - {createHeading(pxtable, tableMeta, headingDataCellCodes)} - - {useMemo( - () => - createRows( - pxtable, - tableMeta, - headingDataCellCodes, - isMobile, - contentVarIndex, - contentsVariableDecimals, - ), - [ - pxtable, - tableMeta, - headingDataCellCodes, - isMobile, - contentVarIndex, - contentsVariableDecimals, - ], - )} - -
+
+ headingRows} + itemContent={(_index, row) => row.cells} + computeItemKey={(index) => index.toString()} + /> +
); });