Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOP-5124: reconstruct table data using tanstack tables #1316

Merged
merged 12 commits into from
Dec 5, 2024
197 changes: 126 additions & 71 deletions src/components/ListTable.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { Cell, HeaderCell, HeaderRow, Row, Table, TableBody, TableHead } from '@leafygreen-ui/table';
import {
Cell,
HeaderCell,
flexRender,
HeaderRow,
Row,
Table,
TableBody,
TableHead,
useLeafyGreenTable,
} from '@leafygreen-ui/table';
import { palette } from '@leafygreen-ui/palette';
import { css, cx } from '@leafygreen-ui/emotion';
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
import { Theme } from '@leafygreen-ui/lib';
import { theme } from '../theme/docsTheme';
import { AncestorComponentContextProvider, useAncestorComponentContext } from '../context/ancestor-components-context';
import ComponentFactory from './ComponentFactory';
Expand Down Expand Up @@ -62,6 +70,14 @@ const baseCellStyle = css`
const bodyCellStyle = css`
overflow-wrap: anywhere;
word-break: break-word;
align-content: flex-start;

& > div {
min-height: unset;
max-height: unset;
flex-direction: column;
align-items: flex-start;
}

*,
p,
Expand All @@ -70,13 +86,14 @@ const bodyCellStyle = css`
}

// Target any nested components (paragraphs, admonitions, tables) and any paragraphs within those nested components
& > div > div > *,
& > div > *,
& > div > div p {
margin: 0 0 12px;
}

// Prevent extra margin below last element (such as when multiple paragraphs are present)
& > div > div > *:last-child {
& > div > div *:last-child,
& > div > *:last-child {
margin-bottom: 0;
}
`;
Expand All @@ -85,6 +102,7 @@ const headerCellStyle = css`
line-height: 24px;
font-weight: 600;
font-size: ${theme.fontSize.small};
width: auto;
`;

const stubCellStyle = css`
Expand Down Expand Up @@ -159,43 +177,92 @@ const includesNestedTable = (rows) => {
return rows.some((row) => checkNodeForTable(row));
};

const ListTableRow = ({ row = [], stubColumnCount, siteTheme, className, ...rest }) => (
<Row className={className}>
{row.map((cell, colIndex) => {
const skipPTag = hasOneChild(cell.children);
const contents = cell.children.map((child, i) => (
<ComponentFactory {...rest} key={`${colIndex}-${i}`} nodeData={child} skipPTag={skipPTag} />
));

const isStub = colIndex <= stubColumnCount - 1;
const role = isStub ? 'rowheader' : null;

return (
<Cell key={colIndex} className={cx(baseCellStyle, bodyCellStyle, isStub && stubCellStyle)} role={role}>
{/* Wrap in div to ensure contents are structured properly */}
<div>{contents}</div>
</Cell>
const generateColumns = (headerRow, bodyRows) => {
if (!headerRow?.children) {
// generate columns from bodyRows
const flattenedRows = bodyRows.map((bodyRow) => bodyRow.children[0].children);
const maxColumns = Math.max(...flattenedRows.map((row) => row.length));
const res = [];
seungpark marked this conversation as resolved.
Show resolved Hide resolved
for (let colIndex = 0; colIndex < maxColumns; colIndex++) {
res.push({
id: `column-${colIndex}`,
accessorKey: `column-${colIndex}`,
header: '',
});
}
return res;
}

return headerRow.children.map((listItemNode, index) => {
const skipPTag = hasOneChild(listItemNode.children);
return {
id: `column-${index}`,
accessorKey: `column-${index}`,
header: (
<>
{listItemNode.children.map((childNode, index) => (
<ComponentFactory key={index} nodeData={childNode} skipPTag={skipPTag} />
))}
</>
),
};
});
};

const generateRowsData = (bodyRowNodes, columns) => {
const rowNodes = bodyRowNodes.map((node) => node?.children[0]?.children ?? []);
const rows = rowNodes.map((rowNode) => {
return rowNode.reduce((res, columnNode, colIndex) => {
res[columns[colIndex].accessorKey] = (
<>
{columnNode.children.map((cellNode, index) => (
<ComponentFactory key={index} nodeData={cellNode} />
))}
</>
);
})}
</Row>
);

ListTableRow.propTypes = {
row: PropTypes.arrayOf(PropTypes.object),
stubColumnCount: PropTypes.number.isRequired,
siteTheme: PropTypes.oneOf(Object.values(Theme)).isRequired,
return res;
}, {});
});

return rows;
};

const ListTable = ({ nodeData: { children, options }, ...rest }) => {
const ancestors = useAncestorComponentContext();
const { theme: siteTheme } = useDarkMode();
const headerRowCount = parseInt(options?.['header-rows'], 10) || 0;
const stubColumnCount = parseInt(options?.['stub-columns'], 10) || 0;
const bodyRows = children[0].children.slice(headerRowCount);
const columnCount = bodyRows[0].children[0].children.length;
const headerRowCount = parseInt(options?.['header-rows'], 10) || 0;

// Check if :header-rows: 0 is specified or :header-rows: is omitted
const headerRows = headerRowCount > 0 ? children[0].children[0].children.slice(0, headerRowCount) : [];
const headerRows = useMemo(() => {
const MAX_HEADER_ROW = 1;
return headerRowCount > 0
? children[0].children[0].children.slice(0, Math.min(MAX_HEADER_ROW, headerRowCount))
: [];
}, [children, headerRowCount]);

const bodyRows = useMemo(() => {
return children[0].children.slice(headerRowCount);
}, [children, headerRowCount]);

// get all ID's for elements within header, or first two rows of body
const firstHeaderRowChildren = headerRows[0]?.children ?? [];
const elmIdsForScroll = getReferenceIds(firstHeaderRowChildren.concat(bodyRows.slice(0, 3)));

const hasNestedTable = useMemo(() => includesNestedTable(bodyRows), [bodyRows]);
const noTableNesting = !hasNestedTable && !ancestors?.table;
const shouldAlternateRowColor = noTableNesting && bodyRows.length > 4;

const tableRef = useRef();
const columns = useMemo(() => generateColumns(headerRows[0], bodyRows), [bodyRows, headerRows]);
const data = useMemo(() => generateRowsData(bodyRows, columns), [bodyRows, columns]);
const table = useLeafyGreenTable({
containerRef: tableRef,
columns: columns,
data: data,
});
const { rows } = table.getRowModel();

const columnCount = columns.length;

let widths = null;
const customWidths = options?.widths;
Expand All @@ -207,14 +274,6 @@ const ListTable = ({ nodeData: { children, options }, ...rest }) => {
}
}

// get all ID's for elements within header, or first two rows of body
const firstHeaderRowChildren = headerRows[0]?.children ?? [];
const elmIdsForScroll = getReferenceIds(firstHeaderRowChildren.concat(bodyRows.slice(0, 3)));

const hasNestedTable = useMemo(() => includesNestedTable(bodyRows), [bodyRows]);
const noTableNesting = !hasNestedTable && !ancestors?.table;
const shouldAlternateRowColor = noTableNesting && bodyRows.length > 4;

return (
<AncestorComponentContextProvider component={'table'}>
{elmIdsForScroll.map((id) => (
Expand All @@ -227,6 +286,7 @@ const ListTable = ({ nodeData: { children, options }, ...rest }) => {
customWidth: options?.width,
})
)}
ref={tableRef}
shouldAlternateRowColor={shouldAlternateRowColor}
>
{widths && (
Expand All @@ -237,37 +297,32 @@ const ListTable = ({ nodeData: { children, options }, ...rest }) => {
</colgroup>
)}
<TableHead className={cx(theadStyle)}>
{headerRows.map((row, rowIndex) => (
<HeaderRow key={rowIndex} data-testid="leafygreen-ui-header-row">
{row.children.map((cell, colIndex) => {
const skipPTag = hasOneChild(cell.children);
{headerRowCount > 0 &&
table.getHeaderGroups().map((headerGroup) => (
<HeaderRow key={headerGroup.id} data-testid="leafygreen-ui-header-row">
{headerGroup.headers.map((header) => {
return (
<HeaderCell className={cx(baseCellStyle, headerCellStyle)} key={header.id} header={header}>
{flexRender(header.column.columnDef.header, header.getContext())}
</HeaderCell>
);
})}
</HeaderRow>
))}
</TableHead>
<TableBody>
{rows.map((row) => (
<Row key={row.id} row={row} className={cx(shouldAlternateRowColor && zebraStripingStyle)}>
{row.getVisibleCells().map((cell, colIndex) => {
const isStub = colIndex <= stubColumnCount - 1;
const role = isStub ? 'rowheader' : null;
return (
<HeaderCell
className={cx(baseCellStyle, headerCellStyle)}
key={`${rowIndex}-${colIndex}`}
role="columnheader"
>
<div>
{cell.children.map((child, i) => (
<ComponentFactory {...rest} key={i} nodeData={child} skipPTag={skipPTag} />
))}
</div>
</HeaderCell>
<Cell key={cell.id} className={cx(baseCellStyle, bodyCellStyle, isStub && stubCellStyle)} role={role}>
{cell.renderValue()}
</Cell>
);
})}
</HeaderRow>
))}
</TableHead>
<TableBody>
{bodyRows.map((row, i) => (
<ListTableRow
key={i}
{...rest}
stubColumnCount={stubColumnCount}
row={row.children?.[0]?.children}
siteTheme={siteTheme}
className={shouldAlternateRowColor && zebraStripingStyle}
/>
</Row>
))}
</TableBody>
</Table>
Expand Down
Loading
Loading