Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion packages/pxweb2-ui/src/lib/components/Table/Table.spec.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<table>
<thead>{fixedHeaderContent?.()}</thead>
<tbody>
{data.map((item, index) => (
<tr key={getRowKey(item, index)}>{itemContent(index, item)}</tr>
))}
</tbody>
</table>
),
}));

import Table from './Table';
import { pxTable } from './testData';
Expand Down
129 changes: 101 additions & 28 deletions packages/pxweb2-ui/src/lib/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 }) => (
<table
style={style}
className={context.tableClassName}
aria-label={context.tableLabel}
>
{children}
</table>
),
TableRow: ({ item, ...props }) => (
<tr {...props} className={item.className} />
),
};

export const Table = memo(function Table({
pxtable,
isMobile,
className = '',
}: TableProps) {
const tableHostRef = useRef<HTMLDivElement>(null);
const [customScrollParent, setCustomScrollParent] =
useState<HTMLElement | null>(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;
Expand Down Expand Up @@ -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 (
<table
className={cl(classes.table, classes[`bodyshort-medium`]) + cssClasses}
aria-label={pxtable.metadata.label}
>
<thead>{createHeading(pxtable, tableMeta, headingDataCellCodes)}</thead>
<tbody>
{useMemo(
() =>
createRows(
pxtable,
tableMeta,
headingDataCellCodes,
isMobile,
contentVarIndex,
contentsVariableDecimals,
),
[
pxtable,
tableMeta,
headingDataCellCodes,
isMobile,
contentVarIndex,
contentsVariableDecimals,
],
)}
</tbody>
</table>
<div ref={tableHostRef}>
<TableVirtuoso
useWindowScroll={!customScrollParent}
customScrollParent={customScrollParent ?? undefined}
data={rowData}
context={{
tableClassName:
cl(classes.table, classes[`bodyshort-medium`]) + cssClasses,
tableLabel: pxtable.metadata.label,
}}
components={tableVirtuosoComponents}
fixedHeaderContent={() => headingRows}
itemContent={(_index, row) => row.cells}
computeItemKey={(index) => index.toString()}
/>
</div>
);
});

Expand Down
Loading