Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.3.0

- Add optional `rowKey` prop to `List` and add optional `rowKey`/`columnKey` prop to `Grid`.

## 2.2.7

- Fixed a problem with project logo not displaying correctly in the README for the Firefox browser.
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ This may be used to (re)scroll a row into view.</p>
<td>overscanCount</td>
<td><p>How many additional rows to render outside of the visible area.
This can reduce visual flickering near the edges of a list when scrolling.</p>
</td>
</tr>
<tr>
<td>rowKey</td>
<td><p>Lists use the row index as a <code>key</code> by default.
This prop can provide a custom <code>key</code> value.</p>
<p>ℹ️ Custom keys can ensure better UX for sortable or filterable lists,
particularly if your row components are stateful.
Refer to the <a href="https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key">React documentation</a> for more info.</p>
<p>⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always <code>useCallback</code> for this prop; do not use an inline function.</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -267,6 +278,17 @@ The grid of cells will fill the height and width defined by this style.</p>
<td>children</td>
<td><p>Additional content to be rendered within the grid (above cells).
This property can be used to render things like overlays or tooltips.</p>
</td>
</tr>
<tr>
<td>columnKey</td>
<td><p>Grids use the column index as a <code>key</code> by default.
This prop can be used along with the <code>rowKey</code> prop to provide a custom <code>key</code> value.</p>
<p>ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
particularly if your cell components are stateful.
Refer to the <a href="https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key">React documentation</a> for more info.</p>
<p>⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always <code>useCallback</code> for this prop; do not use an inline function.</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -302,6 +324,17 @@ This may be used to (re)scroll a cell into view.</p>
<td>overscanCount</td>
<td><p>How many additional rows/columns to render outside of the visible area.
This can reduce visual flickering near the edges of a grid when scrolling.</p>
</td>
</tr>
<tr>
<td>rowKey</td>
<td><p>Grids use the row index as a <code>key</code> by default.
This prop can be used along with the <code>columnKey</code> prop to provide a custom <code>key</code> value.</p>
<p>ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
particularly if your cell components are stateful.
Refer to the <a href="https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key">React documentation</a> for more info.</p>
<p>⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always <code>useCallback</code> for this prop; do not use an inline function.</p>
</td>
</tr>
<tr>
Expand Down
143 changes: 142 additions & 1 deletion lib/components/grid/Grid.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen } from "@testing-library/react";
import { createRef, useLayoutEffect } from "react";
import { act, createRef, useEffect, useLayoutEffect, useRef } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { EMPTY_OBJECT } from "../../../src/constants";
import {
Expand Down Expand Up @@ -76,6 +76,147 @@ describe("Grid", () => {
expect(items).toHaveLength(24);
});

describe("custom keys", () => {
test("are called for every rendered column and row", () => {
const columnKey = vi.fn(({ columnIndex }) => columnIndex);
const rowKey = vi.fn(({ rowIndex }) => rowIndex);

render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnKey={columnKey}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
rowKey={rowKey}
/>
);

for (let index = 0; index < 4; index++) {
expect(columnKey).toHaveBeenCalledWith({
columnIndex: index,
data: EMPTY_OBJECT,
rowIndex: 0
});
expect(columnKey).toHaveBeenCalledWith({
columnIndex: index,
data: EMPTY_OBJECT,
rowIndex: 1
});
}
for (let index = 0; index < 2; index++) {
expect(rowKey).toHaveBeenCalledWith({
data: EMPTY_OBJECT,
rowIndex: index
});
}
});

test("preserves state when list order changes", async () => {
let data = [
[1, 2, 3],
[4, 5, 6]
];

const columnKey = vi.fn(
({ columnIndex, rowIndex }) => data[rowIndex][columnIndex]
);
const rowKey = vi.fn(({ rowIndex }) => data[rowIndex][0]);
const renderLog: string[] = [];

function LocalCellComponent({
columnIndex,
data,
rowIndex,
style
}: CellComponentProps<{ data: number[][] }>) {
const id = data[rowIndex][columnIndex];

const idDuringMountRef = useRef(id);

useEffect(() => {
if (idDuringMountRef.current === id) {
renderLog.push(
`row: ${rowIndex}, column: ${columnIndex}, id: ${id}`
);
return;
}

throw Error(
`Expected id "${idDuringMountRef.current}" but was "${id}"`
);
});

return <div role="listitem" style={style} />;
}

const { rerender } = render(
<Grid
cellComponent={LocalCellComponent}
cellProps={{ data }}
columnCount={3}
columnKey={columnKey}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
rowKey={rowKey}
/>
);

expect(columnKey).toHaveBeenCalled();
expect(rowKey).toHaveBeenCalled();
expect(renderLog).toMatchInlineSnapshot(`
[
"row: 0, column: 0, id: 1",
"row: 0, column: 1, id: 2",
"row: 0, column: 2, id: 3",
"row: 1, column: 0, id: 4",
"row: 1, column: 1, id: 5",
"row: 1, column: 2, id: 6",
]
`);

renderLog.splice(0);
columnKey.mockReset();
rowKey.mockReset();

await act(async () => {
data = [...data.reverse()];

rerender(
<Grid
cellComponent={LocalCellComponent}
cellProps={{ data }}
columnCount={3}
columnKey={columnKey}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
rowKey={rowKey}
/>
);
});

expect(columnKey).toHaveBeenCalled();
expect(rowKey).toHaveBeenCalled();
expect(renderLog).toMatchInlineSnapshot(`
[
"row: 0, column: 0, id: 4",
"row: 0, column: 1, id: 5",
"row: 0, column: 2, id: 6",
"row: 1, column: 0, id: 1",
"row: 1, column: 1, id: 2",
"row: 1, column: 2, id: 3",
]
`);
});
});

describe("cell sizes", () => {
test("type: number (px)", () => {
const { container } = render(
Expand Down
27 changes: 25 additions & 2 deletions lib/components/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function Grid<
children,
className,
columnCount,
columnKey,
columnWidth,
defaultHeight = 0,
defaultWidth = 0,
Expand All @@ -42,6 +43,7 @@ export function Grid<
overscanCount = 3,
rowCount,
rowHeight,
rowKey,
style,
tagName = "div" as TagName,
...rest
Expand Down Expand Up @@ -248,7 +250,15 @@ export function Grid<
role: "gridcell"
}}
columnIndex={columnIndex}
key={columnIndex}
key={
columnKey
? columnKey({
columnIndex,
data: cellProps,
rowIndex
})
: columnIndex
}
rowIndex={rowIndex}
style={{
position: "absolute",
Expand All @@ -263,7 +273,18 @@ export function Grid<
}

children.push(
<div key={rowIndex} role="row" aria-rowindex={rowIndex + 1}>
<div
key={
rowKey
? rowKey({
data: cellProps,
rowIndex
})
: rowIndex
}
role="row"
aria-rowindex={rowIndex + 1}
>
{columns}
</div>
);
Expand All @@ -274,12 +295,14 @@ export function Grid<
CellComponent,
cellProps,
columnCount,
columnKey,
columnStartIndexOverscan,
columnStopIndexOverscan,
getColumnBounds,
getRowBounds,
isRtl,
rowCount,
rowKey,
rowStartIndexOverscan,
rowStopIndexOverscan
]);
Expand Down
30 changes: 30 additions & 0 deletions lib/components/grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ export type GridProps<
*/
columnCount: number;

/**
* Grids use the column index as a `key` by default.
* This prop can be used along with the `rowKey` prop to provide a custom `key` value.
*
* ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
* particularly if your cell components are stateful.
* Refer to the [React documentation](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key) for more info.
*
* ⚠️ This prop cannot be auto-memoized because it is called during render.
* It is important to always `useCallback` for this prop; do not use an inline function.
*/
columnKey?: (args: {
columnIndex: number;
data: CellProps;
rowIndex: number;
}) => React.Key;

/**
* Column width; the following formats are supported:
* - number of pixels (number)
Expand Down Expand Up @@ -201,6 +218,19 @@ export type GridProps<
| string
| ((index: number, cellProps: CellProps) => number);

/**
* Grids use the row index as a `key` by default.
* This prop can be used along with the `columnKey` prop to provide a custom `key` value.
*
* ℹ️ Custom keys can ensure better UX for sortable or filterable grids,
* particularly if your cell components are stateful.
* Refer to the [React documentation](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key) for more info.
*
* ⚠️ This prop cannot be auto-memoized because it is called during render.
* It is important to always `useCallback` for this prop; do not use an inline function.
*/
rowKey?: (args: { data: CellProps; rowIndex: number }) => React.Key;

/**
* Optional CSS properties.
* The grid of cells will fill the height and width defined by this style.
Expand Down
Loading
Loading