From f05de350fdf2e16e4052044c61aaf854d6ac03af Mon Sep 17 00:00:00 2001 From: btmnk Date: Fri, 21 Mar 2025 16:05:26 +0100 Subject: [PATCH] [table] Adds classNames and support for dynamic row height --- core/src/components/Table/Table.module.css | 8 +++ core/src/components/Table/Table.tsx | 48 ++++++++++++--- .../components/TableCell/TableCell.module.css | 9 +-- .../Table/components/TableCell/TableCell.tsx | 13 ++--- .../components/TableRow/TableRow.module.css | 6 +- .../Table/components/TableRow/TableRow.tsx | 43 +++++++++++--- .../components/Table/hooks/useTableProps.ts | 26 ++++++--- .../Table/interface/TableClassNames.ts | 17 ++++++ .../components/Table/interface/TableProps.ts | 20 +++++-- .../Table/stories/Table.stories.module.css | 34 +++++++++++ .../Table/stories/Table.stories.tsx | 58 +++++++++++++++++-- .../Table/stories/util/generateData.tsx | 19 +++++- docs/src/app/docs/components/table/page.mdx | 19 +++--- .../tests/Table/DefaultTable.story.module.css | 3 + playwright/tests/Table/DefaultTable.story.tsx | 13 +++-- playwright/tests/Table/Table.spec.tsx | 21 +++++-- 16 files changed, 281 insertions(+), 76 deletions(-) create mode 100644 core/src/components/Table/interface/TableClassNames.ts create mode 100644 playwright/tests/Table/DefaultTable.story.module.css diff --git a/core/src/components/Table/Table.module.css b/core/src/components/Table/Table.module.css index f8ecd2c..2412e9f 100644 --- a/core/src/components/Table/Table.module.css +++ b/core/src/components/Table/Table.module.css @@ -14,6 +14,7 @@ --rex-table-cell-hover-background-color: #efefef; --rex-table-cell-active-background-color: #dfdfdf; --rex-table-cell-selected-background-color: #c4d9ef; + --rex-table-cell-selected-even-background-color: #b3cae2; --rex-table-cell-selected-hover-background-color: #b2c9e2; --rex-table-cell-selected-active-background-color: #9eb5ce; --rex-table-cell-padding: 4px 8px; @@ -41,6 +42,13 @@ } } + & .noDataContent { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + &.withTableBorders { --rex-table-border-width: 1px; } diff --git a/core/src/components/Table/Table.tsx b/core/src/components/Table/Table.tsx index bdd63ff..a227cb4 100644 --- a/core/src/components/Table/Table.tsx +++ b/core/src/components/Table/Table.tsx @@ -1,6 +1,6 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import clsx from "clsx"; -import { type ElementRef, useRef } from "react"; +import { type ElementRef, useCallback, useMemo, useRef } from "react"; import { Scrollbar } from ".."; import styles from "./Table.module.css"; import { TableHeader } from "./components/TableHeader/TableHeader"; @@ -8,6 +8,7 @@ import { TableRow } from "./components/TableRow/TableRow"; import { useIsScrolled } from "./hooks/useIsScrolled"; import { useTableProps } from "./hooks/useTableProps"; import type { DefaultTableRow } from "./interface/DefaultTableRow"; +import type { TableRowClassNames } from "./interface/TableClassNames"; import type { TableProps } from "./interface/TableProps"; const Table = (props: TableProps) => { @@ -17,18 +18,32 @@ const Table = (props: TableProps) => { const isScrolled = useIsScrolled(viewportRef); const hasHeader = tableProps.columns.some((column) => Boolean(column.header)); + const estimateSize = useCallback( + (index: number) => { + return tableProps.estimateRowHeight?.(tableProps.data[index], index) ?? 36; + }, + [tableProps.estimateRowHeight, tableProps.data], + ); + const virtualizer = useVirtualizer({ count: tableProps.data.length, getScrollElement: () => viewportRef.current, - estimateSize: (index) => tableProps.getRowHeight(tableProps.data[index], index), + estimateSize, overscan: tableProps.overscan, }); const virtualizedItems = virtualizer.getVirtualItems(); const topOffset = virtualizedItems[0]?.start ?? 0; + const showNoDataContent = Boolean(tableProps.data.length === 0 && tableProps.noDataContent); + + const headerRef = useRef(null); + const headerHeight = headerRef.current?.clientHeight ?? 0; + const totalHeight = virtualizer.getTotalSize() + headerHeight; + const containerClasses = clsx( tableProps.className, + tableProps.classNames?.scrollContainer, styles.container, isScrolled && styles.scrolled, tableProps.borders.table && styles.withTableBorders, @@ -37,13 +52,24 @@ const Table = (props: TableProps) => { tableProps.stickyHeader && styles.stickyHeader, ); + const rowClassNames: TableRowClassNames = useMemo( + () => ({ + tableRow: tableProps.classNames?.tableRow, + tableCell: tableProps.classNames?.tableCell, + tableCellContent: tableProps.classNames?.tableCellContent, + }), + [tableProps.classNames], + ); + + const noDataContentClasses = clsx(styles.noDataContent, tableProps.classNames?.noDataContent); + return ( -
- +
+
{hasHeader && ( - - + + {tableProps.columns.map((column) => { return ; })} @@ -51,8 +77,9 @@ const Table = (props: TableProps) => { )} - + + {virtualizedItems.map((virtualItem) => { const row = tableProps.data[virtualItem.index]; const rowId = tableProps.getRowId(row); @@ -61,20 +88,23 @@ const Table = (props: TableProps) => { ); })}
+ + {showNoDataContent &&
{tableProps.noDataContent}
}
); }; diff --git a/core/src/components/Table/components/TableCell/TableCell.module.css b/core/src/components/Table/components/TableCell/TableCell.module.css index aa1aab8..245f589 100644 --- a/core/src/components/Table/components/TableCell/TableCell.module.css +++ b/core/src/components/Table/components/TableCell/TableCell.module.css @@ -13,15 +13,10 @@ } & .content { + display: flex; + align-items: center; padding: var(--rex-table-cell-padding); box-sizing: border-box; - - & .inner { - height: 100%; - display: flex; - align-items: center; - overflow: hidden; - } } } } diff --git a/core/src/components/Table/components/TableCell/TableCell.tsx b/core/src/components/Table/components/TableCell/TableCell.tsx index 30e0c77..7c1ce01 100644 --- a/core/src/components/Table/components/TableCell/TableCell.tsx +++ b/core/src/components/Table/components/TableCell/TableCell.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import { useCellValue } from "../../hooks/useCellValue"; import type { DefaultTableRow } from "../../interface/DefaultTableRow"; +import type { TableCellClassNames } from "../../interface/TableClassNames"; import type { TableColumn } from "../../interface/TableColumn"; import styles from "./TableCell.module.css"; @@ -8,29 +9,27 @@ export type TableCellProps = { row: TRow; column: TableColumn; isLast: boolean; - height: string | number; isSelected: boolean; - classNames?: string; + classNames?: TableCellClassNames; }; const TableCell = (props: TableCellProps) => { - const { row, column, height, isSelected, classNames } = props; + const { row, column, isSelected, classNames } = props; const cellValue = useCellValue({ row, column, isSelected }); let width = column.width ?? 0; if (width === "content") width = "0.1%"; - const cellClasses = clsx(styles.cell, classNames); + const cellClasses = clsx(styles.cell, classNames?.tableCell); + const contentClasses = clsx(styles.content, classNames?.tableCellContent); return ( -
-
{cellValue}
-
+
{cellValue}
); }; diff --git a/core/src/components/Table/components/TableRow/TableRow.module.css b/core/src/components/Table/components/TableRow/TableRow.module.css index 19c467f..1ad9a68 100644 --- a/core/src/components/Table/components/TableRow/TableRow.module.css +++ b/core/src/components/Table/components/TableRow/TableRow.module.css @@ -5,34 +5,30 @@ &:hover { --rex-table-cell-background-color: var(--rex-table-cell-hover-background-color); --rex-table-cell-even-background-color: var(--rex-table-cell-hover-background-color); - --rex-table-cell-border-color: var(--rex-table-cell-hover-border-color); } &:active { --rex-table-cell-background-color: var(--rex-table-cell-active-background-color); --rex-table-cell-even-background-color: var(--rex-table-cell-active-background-color); - --rex-table-cell-border-color: var(--rex-table-cell-active-border-color); } } &.selected { --rex-table-cell-background-color: var(--rex-table-cell-selected-background-color); - + --rex-table-cell-even-background-color: var(--rex-table-cell-selected-even-background-color); --rex-table-cell-border-color: var(--rex-table-cell-selected-border-color); &:hover { --rex-table-cell-background-color: var(--rex-table-cell-selected-hover-background-color); --rex-table-cell-even-background-color: var(--rex-table-cell-selected-hover-background-color); - --rex-table-cell-border-color: var(--rex-table-cell-selected-hover-border-color); } &:active { --rex-table-cell-background-color: var(--rex-table-cell-selected-active-background-color); --rex-table-cell-even-background-color: var(--rex-table-cell-selected-active-background-color); - --rex-table-cell-border-color: var(--rex-table-cell-selected-active-border-color); } } diff --git a/core/src/components/Table/components/TableRow/TableRow.tsx b/core/src/components/Table/components/TableRow/TableRow.tsx index 3f316a1..43014a8 100644 --- a/core/src/components/Table/components/TableRow/TableRow.tsx +++ b/core/src/components/Table/components/TableRow/TableRow.tsx @@ -1,5 +1,8 @@ +import type { VirtualItem } from "@tanstack/react-virtual"; import clsx from "clsx"; +import { useMemo } from "react"; import type { DefaultTableRow } from "../../interface/DefaultTableRow"; +import type { TableCellClassNames, TableRowClassNames } from "../../interface/TableClassNames"; import type { TableColumn } from "../../interface/TableColumn"; import type { OnDeselectRow, OnSelectRow } from "../../interface/TableProps"; import { TableCell } from "../TableCell/TableCell"; @@ -7,21 +10,32 @@ import styles from "./TableRow.module.css"; export type TableRowProps = { id: string | number; - index: number; row: TRow; columns: TableColumn[]; - height: string | number; + virtualItem: VirtualItem; striped: boolean; isSelected: boolean; + classNames?: TableRowClassNames; + measureElement: (node: Element | null | undefined) => void; onSelectRow: OnSelectRow | undefined; onDeselectRow: OnDeselectRow | undefined; }; const TableRow = (props: TableRowProps) => { - const { id, index, row, columns, height, striped, isSelected, onSelectRow, onDeselectRow } = - props; + const { + id, + virtualItem, + row, + columns, + striped, + isSelected, + classNames, + measureElement, + onSelectRow, + onDeselectRow, + } = props; - const isStriped = striped && index % 2 === 0; + const isStriped = striped && virtualItem.index % 2 === 0; const handleRowClick = () => { if (isSelected) { @@ -33,14 +47,28 @@ const TableRow = (props: TableRowProps) => { const selectable = (onSelectRow && !isSelected) || (onDeselectRow && isSelected); const rowClasses = clsx( + classNames?.tableRow, styles.row, selectable && styles.selectable, isSelected && styles.selected, isStriped && styles.striped, ); + const cellClasses: TableCellClassNames = useMemo( + () => ({ + tableCell: clsx(styles.rowCell, classNames?.tableCell), + tableCellContent: classNames?.tableCellContent, + }), + [classNames], + ); + return ( - + {columns.map((column, index) => { return ( (props: TableRowProps) => { column={column} row={row} isSelected={isSelected} - height={height} isLast={columns.length - 1 === index} - classNames={styles.rowCell} + classNames={cellClasses} /> ); })} diff --git a/core/src/components/Table/hooks/useTableProps.ts b/core/src/components/Table/hooks/useTableProps.ts index e9290f3..936b5b0 100644 --- a/core/src/components/Table/hooks/useTableProps.ts +++ b/core/src/components/Table/hooks/useTableProps.ts @@ -1,8 +1,10 @@ +import type { ReactNode } from "react"; import type { DefaultTableRow } from "../interface/DefaultTableRow"; import type { TableBorders } from "../interface/TableBorders"; +import type { TableClassNames } from "../interface/TableClassNames"; import type { TableColumn } from "../interface/TableColumn"; import type { - GetRowHeight, + EstimateRowHeight, GetRowId, OnDeselectRow, OnSelectRow, @@ -18,11 +20,13 @@ export type ComposedTableProps = { stripedRows: boolean; borders: TableBorders; selectedRows: TableRowId[]; - className?: string; + noDataContent?: ReactNode; + className: string | undefined; + classNames: TableClassNames | undefined; getRowId: GetRowId; - getRowHeight: GetRowHeight; - onSelectRow?: OnSelectRow; - onDeselectRow?: OnDeselectRow; + estimateRowHeight: EstimateRowHeight | undefined; + onSelectRow: OnSelectRow | undefined; + onDeselectRow: OnDeselectRow | undefined; divProps: React.DetailedHTMLProps, HTMLDivElement>; }; @@ -31,13 +35,15 @@ export const useTableProps = (props: TableProps(props: TableProps 36), + estimateRowHeight, stickyHeader: stickyHeader ?? true, stripedRows: stripedRows ?? false, borders: _borders, selectedRows: _selectedRows, - onSelectRow: onSelectRow, - onDeselectRow: onDeselectRow, + noDataContent, + onSelectRow, + onDeselectRow, className, + classNames, divProps: divProps, } as const satisfies ComposedTableProps; }; diff --git a/core/src/components/Table/interface/TableClassNames.ts b/core/src/components/Table/interface/TableClassNames.ts new file mode 100644 index 0000000..ee35c0b --- /dev/null +++ b/core/src/components/Table/interface/TableClassNames.ts @@ -0,0 +1,17 @@ +export interface TableCellClassNames { + tableCell?: string; + tableCellContent?: string; +} + +export interface TableRowClassNames extends TableCellClassNames { + tableRow?: string; +} + +export interface TableClassNames extends TableRowClassNames { + scrollContainer?: string; + root?: string; + table?: string; + tableHeader?: string; + tableBody?: string; + noDataContent?: string; +} diff --git a/core/src/components/Table/interface/TableProps.ts b/core/src/components/Table/interface/TableProps.ts index c646ae0..e50875e 100644 --- a/core/src/components/Table/interface/TableProps.ts +++ b/core/src/components/Table/interface/TableProps.ts @@ -1,10 +1,12 @@ +import type { ReactNode } from "react"; import type { DefaultTableRow } from "./DefaultTableRow"; import type { TableBorders } from "./TableBorders"; +import type { TableClassNames } from "./TableClassNames"; import type { TableColumn } from "./TableColumn"; import type { TableRowId } from "./TableRowId"; export type GetRowId = (row: TRow) => TableRowId; -export type GetRowHeight = (row: TRow, index: number) => number; +export type EstimateRowHeight = (row: TRow, index: number) => number; export type OnSelectRowProps = { row: TRow; id: TableRowId }; export type OnSelectRow = (props: OnSelectRowProps) => void; @@ -17,6 +19,11 @@ export interface TableProps data: TRow[]; columns: TableColumn[]; + /** + * Rendered when data is empty + */ + noDataContent?: ReactNode; + /** * Should return a unique identifier for the corresponding row that doesn't change * @param row the dataset for the corresponding row @@ -25,10 +32,10 @@ export interface TableProps getRowId: GetRowId; /** - * Used to determine the height of each row - * @default 36 + * Used to estimate the row height until its measured after being rendered. + * The closer the estimated value is to the actual row height the smoother the scrolling will appear. */ - getRowHeight?: GetRowHeight; + estimateRowHeight?: EstimateRowHeight; /** * The amount of rows rendered out of view @@ -69,4 +76,9 @@ export interface TableProps * @default false */ stripedRows?: boolean; + + /** + * A set of class names for each element of the table + */ + classNames?: TableClassNames; } diff --git a/core/src/components/Table/stories/Table.stories.module.css b/core/src/components/Table/stories/Table.stories.module.css index e69de29..c539c35 100644 --- a/core/src/components/Table/stories/Table.stories.module.css +++ b/core/src/components/Table/stories/Table.stories.module.css @@ -0,0 +1,34 @@ +.scrollContainer { + padding: 3px; + border: 2px dashed gray; +} + +.table { + background-color: aliceblue; + border-collapse: separate; + border-spacing: 4px; +} + +.tableHeader { + background-color: transparent; + + & th { + border: 0; + } +} + +.tableRow { + background-color: transparent; +} + +.tableCell { + border: 0; +} + +.tableCellContent { + border: 1px dashed #aaa; +} + +.pre { + padding: 25px; +} diff --git a/core/src/components/Table/stories/Table.stories.tsx b/core/src/components/Table/stories/Table.stories.tsx index 201b2da..5a1152f 100644 --- a/core/src/components/Table/stories/Table.stories.tsx +++ b/core/src/components/Table/stories/Table.stories.tsx @@ -6,9 +6,10 @@ import type { DefaultTableRow } from "../interface/DefaultTableRow"; import type { TableColumn } from "../interface/TableColumn"; import type { TableProps } from "../interface/TableProps"; import type { TableRowId } from "../interface/TableRowId"; -import { generateData, genericColumns } from "./util/generateData"; +import styles from "./Table.stories.module.css"; +import { dynamicHeightColumns, generateData, genericColumns } from "./util/generateData"; -const defaultData = generateData({ amount: 500 }); +const defaultData = generateData({ amount: 5000 }); const defaultColumns = genericColumns as TableColumn[]; const headerlessColumns = defaultColumns.map((it) => ({ ...it, header: undefined })); @@ -32,7 +33,7 @@ export default { overscan: 5, stickyHeader: true, borders: { table: true, horizontal: true, vertical: false }, - stripedRows: false, + stripedRows: true, getRowId: (row) => row.id, getRowHeight: () => 50, } as Partial>, @@ -40,13 +41,18 @@ export default { type Story = StoryObj; -const TableWrapper = (args: TableProps) => { +type TableWrapperProps = TableProps & { + height?: number | string; +}; + +const TableWrapper = (args: TableWrapperProps) => { + const { height = 400, ...tableProps } = args; const [selected, setSelected] = useState([]); return ( -
+
setSelected((state) => [...state, id])} onDeselectRow={({ id }) => setSelected((state) => state.toSpliced(state.indexOf(id), 1))} @@ -59,9 +65,49 @@ export const Default: Story = { render: (args) => , }; +export const DynamicHeight: Story = { + render: (args) => ( + []} /> + ), +}; + export const Headerless: Story = { args: { columns: headerlessColumns, }, render: (args) => , }; + +export const Customized: Story = { + render: (args) => ( + + ), +}; + +export const NoData: Story = { + render: (args) => ( + No data! + } + /> + ), +}; + +export const FullHeight: Story = { + render: (args) => , +}; diff --git a/core/src/components/Table/stories/util/generateData.tsx b/core/src/components/Table/stories/util/generateData.tsx index 6e5eca6..414bca4 100644 --- a/core/src/components/Table/stories/util/generateData.tsx +++ b/core/src/components/Table/stories/util/generateData.tsx @@ -54,7 +54,9 @@ export const genericColumns: TableColumn[] = [ { id: "img", header: "Image", - cellRenderer: ({ row }) => lorem picsum, + cellRenderer: ({ row }) => ( + lorem picsum + ), width: "content", }, { @@ -92,3 +94,18 @@ export const genericColumns: TableColumn[] = [ ), }, ]; + +export const dynamicHeightColumns: TableColumn[] = genericColumns.map((col) => { + if (col.id === "description") { + return { + ...col, + cellRenderer: ({ row }) => ( +
+ {row.description} +
+ ), + } satisfies TableColumn; + } + + return col; +}); diff --git a/docs/src/app/docs/components/table/page.mdx b/docs/src/app/docs/components/table/page.mdx index 70fbc1d..a729262 100644 --- a/docs/src/app/docs/components/table/page.mdx +++ b/docs/src/app/docs/components/table/page.mdx @@ -6,15 +6,11 @@ A simple data table with high customizability. > If you're looking for a feature-rich table you might want to have a look at [mantine datatable](https://icflorescu.github.io/mantine-datatable/) or [mantine-react-table](https://www.mantine-react-table.com/) -## Why should I use this table? +## Features -_You probably shouldn't_. - -Tables are often an excuse for displaying huge amounts of complex data without actually thinking about the underlying use case. Before resorting to a table for data visualization it's recommended to first analyze what information the user actually wants to find and then display that as concise as possible. - -If you find yourself at a point where there really is no other solution than using a table for data visualization then you can have a look at this component. It tries to offer most basic table needs in a very simple but also customizable way. - -**This table only covers virtualization and simple layouting.** You will have to implement advanced features like **sorting**, **filtering** etc. yourself. +- Simple semantic table with native table elements +- Virtualization powered by @tanstack/virtual +- Column configurations with custom renderers ## Usage @@ -38,15 +34,15 @@ return ( ## CSS Vars -All table styles are wrapped in a `@layer` so you can easily override styles and css vars somewhere in your css. +All table styles are wrapped in a `@layer` so you can easily override styles and css vars anywhere in your css. ```css :root { --rex-table-header-height: 36px; --rex-table-header-padding: 0px 8px; --rex-table-header-background-color: white; - --rex-table-header-shadow: 0px 0.8px 5.3px rgba(0, 0, 0, 0.02), - 0px 2.7px 17.9px rgba(0, 0, 0, 0.03), 0px 12px 80px rgba(0, 0, 0, 0.05); + --rex-table-header-shadow: 0px 0.8px 5.3px rgba(0, 0, 0, 0.02), 0px 2.7px 17.9px + rgba(0, 0, 0, 0.03), 0px 12px 80px rgba(0, 0, 0, 0.05); --rex-table-border-width: 0px; --rex-table-border-color: #dbdbdb; @@ -56,6 +52,7 @@ All table styles are wrapped in a `@layer` so you can easily override styles and --rex-table-cell-hover-background-color: #efefef; --rex-table-cell-active-background-color: #dfdfdf; --rex-table-cell-selected-background-color: #c4d9ef; + --rex-table-cell-selected-even-background-color: #b3cae2; --rex-table-cell-selected-hover-background-color: #b2c9e2; --rex-table-cell-selected-active-background-color: #9eb5ce; --rex-table-cell-padding: 4px 8px; diff --git a/playwright/tests/Table/DefaultTable.story.module.css b/playwright/tests/Table/DefaultTable.story.module.css new file mode 100644 index 0000000..14340b2 --- /dev/null +++ b/playwright/tests/Table/DefaultTable.story.module.css @@ -0,0 +1,3 @@ +.container { + --rex-table-cell-padding: 0; +} diff --git a/playwright/tests/Table/DefaultTable.story.tsx b/playwright/tests/Table/DefaultTable.story.tsx index 5e9eeea..a2c88c1 100644 --- a/playwright/tests/Table/DefaultTable.story.tsx +++ b/playwright/tests/Table/DefaultTable.story.tsx @@ -1,4 +1,6 @@ +import type { ReactNode } from "react"; import { Table, type TableColumn } from "../../../core/src"; +import styles from "./DefaultTable.story.module.css"; type MyData = { id: number; a: string; b: string; c: number }; @@ -28,10 +30,12 @@ const DefaultTableStory = (props: DefaultTableStoryProps) => { c: index * Math.random() * 100, })); + const defaultRenderer = (value: ReactNode) =>
{value}
; + let columns: TableColumn[] = [ - { id: "a", accessor: "a", header: "A" }, - { id: "b", accessor: "b", header: "B" }, - { id: "c", accessor: "c", header: "C" }, + { id: "a", accessor: "a", cellRenderer: ({ row }) => defaultRenderer(row.a) }, + { id: "b", accessor: "b", cellRenderer: ({ row }) => defaultRenderer(row.b) }, + { id: "c", accessor: "c", cellRenderer: ({ row }) => defaultRenderer(row.c) }, ]; if (withHeaders) { @@ -39,12 +43,11 @@ const DefaultTableStory = (props: DefaultTableStoryProps) => { } return ( -
+
row.id} - getRowHeight={() => rowHeight} stickyHeader={stickyHeader} overscan={overscan} /> diff --git a/playwright/tests/Table/Table.spec.tsx b/playwright/tests/Table/Table.spec.tsx index 222cee6..102d8b9 100644 --- a/playwright/tests/Table/Table.spec.tsx +++ b/playwright/tests/Table/Table.spec.tsx @@ -18,16 +18,29 @@ test.describe("Table", () => { await expect(component.locator("tbody tr")).toHaveCount(4); }); - test("virtualizes rows", async ({ mount }) => { + test("virtualizes rows", async ({ mount, page }) => { const component = await mount( , ); // max height 500 / 50 = 10 rows are visible + 10 overscan - // so at least 20 rows should be visible and at max 25 to give a reasonable margin + // so exactly 20 rows should be visible + 1 overflow row + const initialLastRowIndex = await component + .locator("tbody tr") + .last() + .getAttribute("data-index"); const count = await component.locator("tbody tr").count(); - expect(count).toBeGreaterThan(20); - expect(count).toBeLessThan(25); + expect(count).toBe(21); + expect(initialLastRowIndex).toBe("19"); + + // scroll enough for the next overscan element to be rendered + await component.hover(); + await page.mouse.wheel(0, 51); + await expect + .poll(async () => { + return component.locator("tbody tr").last().getAttribute("data-index"); + }) + .toBe("20"); }); test("renders headers", async ({ mount }) => {