✨(react) add sorting on custom cells
Previously sorting on custom cells was not possible because they were considered as "display cells" by React-Table, which is made for actions columns, which aren't sortable. Closes #296 #100
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import React, { useState } from "react";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { getAllByRole, getByRole } from "@testing-library/dom";
|
||||
import { getAllByRole, getByRole, within } from "@testing-library/dom";
|
||||
import { expect } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { expectPaginationList } from ":/components/Pagination/utils";
|
||||
@@ -418,6 +418,99 @@ describe("<SimpleDataGrid/>", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
it("should render a grid with working sortable custom columns", async () => {
|
||||
const rows = Array.from(Array(23))
|
||||
.map(() => ({
|
||||
id: faker.string.uuid(),
|
||||
firstName: faker.person.firstName(),
|
||||
country: faker.location.country(),
|
||||
}))
|
||||
.sort((a, b) => a.firstName.localeCompare(b.firstName));
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<SimpleDataGrid
|
||||
columns={[
|
||||
{
|
||||
field: "firstName",
|
||||
headerName: "First name",
|
||||
},
|
||||
{
|
||||
field: "country",
|
||||
headerName: "Country",
|
||||
renderCell: ({ row }) => <h1>{row.country}</h1>,
|
||||
},
|
||||
]}
|
||||
rows={rows}
|
||||
defaultPaginationParams={{
|
||||
pageSize: 10,
|
||||
}}
|
||||
defaultSortModel={[
|
||||
{
|
||||
field: "firstName",
|
||||
sort: "asc",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Verify first page rows are sorted by firstName ASC.
|
||||
expect(screen.getAllByRole("row").length).toBe(11);
|
||||
rows.slice(0, 10).forEach((row) => {
|
||||
const element = screen.getByTestId(row.id);
|
||||
const tds = getAllByRole(element, "cell");
|
||||
expect(tds.length).toBe(2);
|
||||
expect(tds[0].textContent).toEqual(row.firstName);
|
||||
});
|
||||
|
||||
// Sort by country ASC.
|
||||
const table = screen.getByRole("table");
|
||||
const ths = getAllByRole(table, "columnheader");
|
||||
expect(ths.length).toBe(2);
|
||||
expect(ths[1].textContent).toContain("Country");
|
||||
expect(ths[1].textContent).not.toContain("arrow_drop_up");
|
||||
expect(ths[1].textContent).not.toContain("arrow_drop_down");
|
||||
await user.click(ths[1].querySelector("div")!);
|
||||
|
||||
// Verify first page rows are sorted by country ASC.
|
||||
await waitFor(() => {
|
||||
expect(ths[1].textContent).toContain("arrow_drop_up");
|
||||
[...rows]
|
||||
.sort((a, b) => a.country.localeCompare(b.country))
|
||||
.slice(0, 10)
|
||||
.forEach((row) => {
|
||||
const element = screen.getByTestId(row.id);
|
||||
const tds = getAllByRole(element, "cell");
|
||||
expect(tds.length).toBe(2);
|
||||
expect(tds[0].textContent).toEqual(row.firstName);
|
||||
within(tds[1]).getByRole("heading", { name: row.country, level: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by country DESC.
|
||||
expect(ths.length).toBe(2);
|
||||
expect(ths[1].textContent).toContain("Country");
|
||||
expect(ths[1].textContent).toContain("arrow_drop_up");
|
||||
expect(ths[1].textContent).not.toContain("arrow_drop_down");
|
||||
await user.click(ths[1].querySelector("div")!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ths[1].textContent).toContain("arrow_drop_down");
|
||||
[...rows]
|
||||
.sort((a, b) => a.country.localeCompare(b.country))
|
||||
.reverse()
|
||||
.slice(0, 10)
|
||||
.forEach((row) => {
|
||||
const element = screen.getByTestId(row.id);
|
||||
const tds = getAllByRole(element, "cell");
|
||||
expect(tds.length).toBe(2);
|
||||
expect(tds[0].textContent).toEqual(row.firstName);
|
||||
within(tds[1]).getByRole("heading", { name: row.country, level: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
it("should render a grid with non-sortable columns", async () => {
|
||||
const rows = Array.from(Array(23))
|
||||
.map(() => ({
|
||||
|
||||
@@ -195,6 +195,7 @@ export const FullServerSide = () => {
|
||||
lastName: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
address: faker.location.streetAddress(),
|
||||
country: faker.location.countryCode(),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
@@ -261,6 +262,24 @@ export const FullServerSide = () => {
|
||||
field: "address",
|
||||
headerName: "Address",
|
||||
},
|
||||
{
|
||||
headerName: "Country",
|
||||
field: "country",
|
||||
renderCell: (context) => {
|
||||
return (
|
||||
<span
|
||||
style={{ display: "flex", alignItems: "center", gap: "8px" }}
|
||||
>
|
||||
<img
|
||||
style={{ height: "24px" }}
|
||||
src={`https://flagsapi.com/${context.row.country}/shiny/64.png`}
|
||||
alt="Flag"
|
||||
/>
|
||||
{context.row.country}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
renderCell: () => (
|
||||
|
||||
@@ -30,12 +30,19 @@ export interface ColumnField {
|
||||
}
|
||||
|
||||
export interface ColumnCustomCell<T extends Row = Row> {
|
||||
id?: string;
|
||||
field: string;
|
||||
renderCell: (params: { row: T }) => React.ReactNode;
|
||||
}
|
||||
export interface ColumnDisplayCell<T extends Row = Row> {
|
||||
id: string;
|
||||
field?: never;
|
||||
renderCell: (params: { row: T }) => React.ReactNode;
|
||||
}
|
||||
|
||||
export type Column<T extends Row = Row> = (
|
||||
| ColumnCustomCell<T>
|
||||
| ColumnDisplayCell<T>
|
||||
| ColumnField
|
||||
) & {
|
||||
headerName?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
CellContext,
|
||||
ColumnDef,
|
||||
createColumnHelper,
|
||||
PaginationState,
|
||||
@@ -23,23 +24,23 @@ export const useHeadlessColumns = <T extends Row>({
|
||||
const { t } = useCunningham();
|
||||
const columnHelper = createColumnHelper<T>();
|
||||
let headlessColumns = columns.map((column) => {
|
||||
// Based on types we can assume that at least one of both is defined.
|
||||
const id = (column.id ?? column.field) as string;
|
||||
const opts = {
|
||||
id: column.renderCell ? column.id : column.id ?? column.field,
|
||||
id,
|
||||
enableSorting: column.enableSorting,
|
||||
header: column.headerName,
|
||||
cell: (info: CellContext<any, any>) => {
|
||||
if (column.renderCell) {
|
||||
return column.renderCell({ row: info.row.original });
|
||||
}
|
||||
return info.cell.getValue();
|
||||
},
|
||||
};
|
||||
if (!column.renderCell) {
|
||||
// The any cast is needed because the type of the accessor is hard-defined on react-table.
|
||||
// On our side we only use string as type for simplicity purpose.
|
||||
if (column.field) {
|
||||
return columnHelper.accessor(column.field as any, opts);
|
||||
}
|
||||
|
||||
return columnHelper.display({
|
||||
...opts,
|
||||
cell: (info) => {
|
||||
return column.renderCell({ row: info.row.original });
|
||||
},
|
||||
});
|
||||
return columnHelper.display(opts);
|
||||
});
|
||||
if (enableRowSelection) {
|
||||
headlessColumns = [
|
||||
|
||||
Reference in New Issue
Block a user