diff --git a/.changeset/tall-glasses-attack.md b/.changeset/tall-glasses-attack.md
new file mode 100644
index 0000000..b59dddd
--- /dev/null
+++ b/.changeset/tall-glasses-attack.md
@@ -0,0 +1,5 @@
+---
+"@openfun/cunningham-react": minor
+---
+
+add sorting on custom columns
diff --git a/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx b/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx
index 85cac9f..e871da7 100644
--- a/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx
+++ b/packages/react/src/components/DataGrid/SimpleDataGrid.spec.tsx
@@ -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("", () => {
});
});
});
+ 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(
+
+ {row.country}
,
+ },
+ ]}
+ rows={rows}
+ defaultPaginationParams={{
+ pageSize: 10,
+ }}
+ defaultSortModel={[
+ {
+ field: "firstName",
+ sort: "asc",
+ },
+ ]}
+ />
+ ,
+ );
+
+ 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(() => ({
diff --git a/packages/react/src/components/DataGrid/index.stories.tsx b/packages/react/src/components/DataGrid/index.stories.tsx
index 0cf2e12..c4a4bf2 100644
--- a/packages/react/src/components/DataGrid/index.stories.tsx
+++ b/packages/react/src/components/DataGrid/index.stories.tsx
@@ -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 (
+
+
+ {context.row.country}
+
+ );
+ },
+ },
{
id: "actions",
renderCell: () => (
diff --git a/packages/react/src/components/DataGrid/index.tsx b/packages/react/src/components/DataGrid/index.tsx
index 0f3edf8..a5003e6 100644
--- a/packages/react/src/components/DataGrid/index.tsx
+++ b/packages/react/src/components/DataGrid/index.tsx
@@ -30,12 +30,19 @@ export interface ColumnField {
}
export interface ColumnCustomCell {
+ id?: string;
+ field: string;
+ renderCell: (params: { row: T }) => React.ReactNode;
+}
+export interface ColumnDisplayCell {
id: string;
+ field?: never;
renderCell: (params: { row: T }) => React.ReactNode;
}
export type Column = (
| ColumnCustomCell
+ | ColumnDisplayCell
| ColumnField
) & {
headerName?: string;
diff --git a/packages/react/src/components/DataGrid/utils.tsx b/packages/react/src/components/DataGrid/utils.tsx
index 48a6276..61e79ad 100644
--- a/packages/react/src/components/DataGrid/utils.tsx
+++ b/packages/react/src/components/DataGrid/utils.tsx
@@ -1,4 +1,5 @@
import {
+ CellContext,
ColumnDef,
createColumnHelper,
PaginationState,
@@ -23,23 +24,23 @@ export const useHeadlessColumns = ({
const { t } = useCunningham();
const columnHelper = createColumnHelper();
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) => {
+ 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 = [