+ ) => (
+
+ {children}
+
+ )
+);
+
+TableCaption.displayName = "utrecht-table__caption";
diff --git a/src/components/utrecht/TableCell.test.tsx b/src/components/utrecht/TableCell.test.tsx
new file mode 100644
index 00000000..6af0a000
--- /dev/null
+++ b/src/components/utrecht/TableCell.test.tsx
@@ -0,0 +1,137 @@
+import { render, screen } from "@testing-library/react";
+import { createRef } from "react";
+import { Table } from "./Table";
+import { TableBody } from "./TableBody";
+import { TableRow } from "./TableRow";
+import { TableCell } from "./TableCell";
+import "@testing-library/jest-dom";
+
+describe("Table cell", () => {
+ /**
+ * The following tests will render an complete table instead of just one standalone table cell,
+ * because React doesn't allow standalone rendering of .
+ *
+ * Since simply using `:only-child` like the other tests doesn't work anymore,
+ * the following tests heavily rely on `useRef()`. (That's also why the test for ForwardRef is
+ * one of the first tests.)
+ */
+ it("renders a cell role element", () => {
+ render(
+
+ );
+
+ const cell = screen.getByRole("cell");
+
+ expect(cell).toBeInTheDocument();
+ expect(cell).toBeVisible();
+ });
+
+ it("supports ForwardRef in React", () => {
+ const ref = createRef();
+
+ const { container } = render(
+
+ );
+
+ const row = screen.getByRole("row");
+ const cell = row.querySelector(":only-child");
+
+ expect(ref.current).toBe(cell);
+ });
+
+ it("renders a td HTML element", () => {
+ const { container } = render(
+
+ );
+ const cell = container.querySelector("td:only-child");
+
+ expect(cell).toBeInTheDocument();
+ });
+
+ it("renders a design system BEM class name", () => {
+ const ref = createRef();
+ render(
+
+ );
+ const cell = ref.current;
+
+ expect(cell).toHaveClass("utrecht-table__cell");
+ });
+
+ it("renders rich text content", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ const cell = ref.current;
+
+ const richText = cell?.querySelector("data");
+
+ expect(richText).toBeInTheDocument();
+ });
+
+ it("can be hidden", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ expect(ref.current).not.toBeVisible();
+ });
+
+ it("can have a custom class name", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ expect(ref.current).toHaveClass("negative");
+ });
+});
diff --git a/src/components/utrecht/TableCell.tsx b/src/components/utrecht/TableCell.tsx
new file mode 100644
index 00000000..32942241
--- /dev/null
+++ b/src/components/utrecht/TableCell.tsx
@@ -0,0 +1,22 @@
+/**
+ * @license EUPL-1.2
+ * Copyright (c) 2022 Robbert Broersma
+ */
+
+import clsx from "clsx";
+import { ForwardedRef, forwardRef, PropsWithChildren, TdHTMLAttributes } from "react";
+
+type TableCellProps = TdHTMLAttributes;
+
+export const TableCell = forwardRef(
+ (
+ { children, className, ...restProps }: PropsWithChildren,
+ ref: ForwardedRef
+ ) => (
+
+ {children}
+ |
+ )
+);
+
+TableCell.displayName = "utrecht-table__cell";
diff --git a/src/components/utrecht/TableFooter.test.tsx b/src/components/utrecht/TableFooter.test.tsx
new file mode 100644
index 00000000..a584a41a
--- /dev/null
+++ b/src/components/utrecht/TableFooter.test.tsx
@@ -0,0 +1,96 @@
+import { render, screen } from "@testing-library/react";
+import { createRef } from "react";
+import { Table } from "./Table";
+import { TableFooter } from "./TableFooter";
+import { TableRow } from "./TableRow";
+import "@testing-library/jest-dom";
+
+fdescribe("Table footer", () => {
+ it("renders a rowgroup role element", () => {
+ render(
+
+ );
+
+ const tableFooter = screen.getByRole("rowgroup");
+
+ expect(tableFooter).toBeInTheDocument();
+ expect(tableFooter).toBeVisible();
+ });
+
+ it("renders an HTML tfoot element", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableFooter = table?.querySelector("tfoot:only-child");
+
+ expect(tableFooter).toBeInTheDocument();
+ });
+
+ it("renders a design system BEM class name", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableFooter = table?.querySelector(":only-child");
+
+ expect(tableFooter).toHaveClass("utrecht-table__footer");
+ });
+
+ it("renders table rows", () => {
+ const { container } = render(
+
+ );
+ const tableFooter = screen.getByRole("rowgroup");
+ const row = tableFooter?.querySelector(":only-child");
+
+ expect(row).toBe(screen.getByRole("row"));
+ });
+
+ it("can be hidden", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableFooter = table?.querySelector(":only-child");
+
+ expect(tableFooter).not.toBeVisible();
+ });
+
+ it("can have a custom class name", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableFooter = table?.querySelector(":only-child");
+
+ expect(tableFooter).toHaveClass("alternate-column-colors");
+ });
+
+ it("supports ForwardRef in React", () => {
+ const ref = createRef();
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableFooter = table?.querySelector(":only-child");
+
+ expect(ref.current).toBe(tableFooter);
+ });
+});
diff --git a/src/components/utrecht/TableFooter.tsx b/src/components/utrecht/TableFooter.tsx
new file mode 100644
index 00000000..40ddb369
--- /dev/null
+++ b/src/components/utrecht/TableFooter.tsx
@@ -0,0 +1,22 @@
+/**
+ * @license EUPL-1.2
+ * Copyright (c) 2022 Robbert Broersma
+ */
+
+import clsx from "clsx";
+import { ForwardedRef, forwardRef, PropsWithChildren, TableHTMLAttributes } from "react";
+
+type TableFooterProps = TableHTMLAttributes;
+
+export const TableFooter = forwardRef(
+ (
+ { children, className, ...restProps }: PropsWithChildren,
+ ref: ForwardedRef
+ ) => (
+
+ {children}
+
+ )
+);
+
+TableFooter.displayName = "utrecht-table__footer";
diff --git a/src/components/utrecht/TableHeader.test.tsx b/src/components/utrecht/TableHeader.test.tsx
new file mode 100644
index 00000000..647f239c
--- /dev/null
+++ b/src/components/utrecht/TableHeader.test.tsx
@@ -0,0 +1,96 @@
+import { render, screen } from "@testing-library/react";
+import { createRef } from "react";
+import { Table } from "./Table";
+import { TableHeader } from "./TableHeader";
+import { TableRow } from "./TableRow";
+import "@testing-library/jest-dom";
+
+fdescribe("Table header", () => {
+ it("renders a rowgroup role element", () => {
+ render(
+
+ );
+
+ const tableHeader = screen.getByRole("rowgroup");
+
+ expect(tableHeader).toBeInTheDocument();
+ expect(tableHeader).toBeVisible();
+ });
+
+ it("renders an HTML thead element", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableHeader = table?.querySelector("thead:only-child");
+
+ expect(tableHeader).toBeInTheDocument();
+ });
+
+ it("renders a design system BEM class name", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableHeader = table?.querySelector(":only-child");
+
+ expect(tableHeader).toHaveClass("utrecht-table__header");
+ });
+
+ it("renders table rows", () => {
+ const { container } = render(
+
+ );
+ const tableHeader = screen.getByRole("rowgroup");
+ const row = tableHeader?.querySelector(":only-child");
+
+ expect(row).toBe(screen.getByRole("row"));
+ });
+
+ it("can be hidden", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableHeader = table?.querySelector(":only-child");
+
+ expect(tableHeader).not.toBeVisible();
+ });
+
+ it("can have a custom class name", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableHeader = table?.querySelector(":only-child");
+
+ expect(tableHeader).toHaveClass("alternate-column-colors");
+ });
+
+ it("supports ForwardRef in React", () => {
+ const ref = createRef();
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableHeader = table?.querySelector(":only-child");
+
+ expect(ref.current).toBe(tableHeader);
+ });
+});
diff --git a/src/components/utrecht/TableHeader.tsx b/src/components/utrecht/TableHeader.tsx
new file mode 100644
index 00000000..dbee5aa6
--- /dev/null
+++ b/src/components/utrecht/TableHeader.tsx
@@ -0,0 +1,22 @@
+/**
+ * @license EUPL-1.2
+ * Copyright (c) 2022 Robbert Broersma
+ */
+
+import clsx from "clsx";
+import { ForwardedRef, forwardRef, PropsWithChildren, TableHTMLAttributes } from "react";
+
+type TableHeaderProps = TableHTMLAttributes;
+
+export const TableHeader = forwardRef(
+ (
+ { children, className, ...restProps }: PropsWithChildren,
+ ref: ForwardedRef
+ ) => (
+
+ {children}
+
+ )
+);
+
+TableHeader.displayName = "utrecht-table__header";
diff --git a/src/components/utrecht/TableHeaderCell.test.tsx b/src/components/utrecht/TableHeaderCell.test.tsx
new file mode 100644
index 00000000..010b5a4a
--- /dev/null
+++ b/src/components/utrecht/TableHeaderCell.test.tsx
@@ -0,0 +1,153 @@
+import { render, screen } from "@testing-library/react";
+import { createRef } from "react";
+import { Table } from "./Table";
+import { TableBody } from "./TableBody";
+import { TableRow } from "./TableRow";
+import { TableHeaderCell } from "./TableHeaderCell";
+import "@testing-library/jest-dom";
+
+describe("Table header cell", () => {
+ /**
+ * The following tests will render an complete table instead of just one standalone table cell,
+ * because React doesn't allow standalone rendering of .
+ *
+ * Since simply using `:only-child` like the other tests doesn't work anymore,
+ * the following tests heavily rely on `useRef()`. (That's also why the test for ForwardRef is
+ * one of the first tests.)
+ */
+ it("renders a columnheader role element", () => {
+ render(
+
+ );
+
+ const cell = screen.getByRole("columnheader");
+
+ expect(cell).toBeInTheDocument();
+ expect(cell).toBeVisible();
+ });
+
+ it("can render a rowheader role element", () => {
+ render(
+
+ );
+
+ const cell = screen.getByRole("rowheader");
+
+ expect(cell).toBeInTheDocument();
+ });
+
+ it("supports ForwardRef in React", () => {
+ const ref = createRef();
+
+ const { container } = render(
+
+ );
+
+ const row = screen.getByRole("row");
+ const cell = row.querySelector(":only-child");
+
+ expect(ref.current).toBe(cell);
+ });
+
+ it("renders a th HTML element", () => {
+ const { container } = render(
+
+ );
+ const cell = container.querySelector("th:only-child");
+
+ expect(cell).toBeInTheDocument();
+ });
+
+ it("renders a design system BEM class name", () => {
+ const ref = createRef();
+ render(
+
+ );
+ const cell = ref.current;
+
+ expect(cell).toHaveClass("utrecht-table__header-cell");
+ });
+
+ it("renders rich text content", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ const cell = ref.current;
+
+ const richText = cell?.querySelector("abbr");
+
+ expect(richText).toBeInTheDocument();
+ });
+
+ it("can be hidden", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ expect(ref.current).not.toBeVisible();
+ });
+
+ it("can have a custom class name", () => {
+ const ref = createRef();
+ render(
+
+ );
+
+ expect(ref.current).toHaveClass("negative");
+ });
+});
diff --git a/src/components/utrecht/TableHeaderCell.tsx b/src/components/utrecht/TableHeaderCell.tsx
new file mode 100644
index 00000000..ac7a82b0
--- /dev/null
+++ b/src/components/utrecht/TableHeaderCell.tsx
@@ -0,0 +1,22 @@
+/**
+ * @license EUPL-1.2
+ * Copyright (c) 2022 Robbert Broersma
+ */
+
+import clsx from "clsx";
+import { ForwardedRef, forwardRef, PropsWithChildren, ThHTMLAttributes } from "react";
+
+type TableHeaderCellProps = ThHTMLAttributes;
+
+export const TableHeaderCell = forwardRef(
+ (
+ { children, className, ...restProps }: PropsWithChildren,
+ ref: ForwardedRef
+ ) => (
+
+ {children}
+ |
+ )
+);
+
+TableHeaderCell.displayName = "utrecht-table__header-cell";
diff --git a/src/components/utrecht/TableRow.test.tsx b/src/components/utrecht/TableRow.test.tsx
new file mode 100644
index 00000000..a065c007
--- /dev/null
+++ b/src/components/utrecht/TableRow.test.tsx
@@ -0,0 +1,106 @@
+import { render, screen } from "@testing-library/react";
+import { createRef } from "react";
+import { Table } from "./Table";
+import { TableHeader } from "./TableHeader";
+import { TableFooter } from "./TableFooter";
+import { TableBody } from "./TableBody";
+import { TableCaption } from "./TableCaption";
+import { TableRow } from "./TableRow";
+import "@testing-library/jest-dom";
+
+fdescribe("Table row", () => {
+ it("renders a table row role element", () => {
+ render(
+
+ );
+
+ const table = screen.getByRole("row");
+
+ expect(table).toBeInTheDocument();
+ expect(table).toBeVisible();
+ });
+
+ it("renders an HTML tr element", () => {
+ const { container } = render(
+
+ );
+
+ const table = container.querySelector(":only-child");
+ const tableBody = table?.querySelector(":only-child");
+ const tableRow = tableBody?.querySelector("tr:only-child");
+
+ expect(tableRow).toBeInTheDocument();
+ });
+
+ it("renders a design system BEM class name", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableBody = table?.querySelector(":only-child");
+ const tableRow = tableBody?.querySelector(":only-child");
+
+ expect(tableRow).toHaveClass("utrecht-table__row");
+ });
+
+ it("can be hidden", () => {
+ const { container } = render(
+
+ );
+ const table = container.querySelector(":only-child");
+ const tableBody = table?.querySelector(":only-child");
+ const tableRow = tableBody?.querySelector(":only-child");
+
+ expect(tableRow).not.toBeVisible();
+ });
+
+ it("can have a custom class name", () => {
+ const { container } = render(
+
+ );
+
+ const table = container.querySelector(":only-child");
+ const tableBody = table?.querySelector(":only-child");
+ const tableRow = tableBody?.querySelector(":only-child");
+
+ expect(tableRow).toHaveClass("odd");
+ });
+
+ it("supports ForwardRef in React", () => {
+ const ref = createRef();
+
+ const { container } = render(
+
+ );
+
+ const table = container.querySelector(":only-child");
+ const tableBody = table?.querySelector(":only-child");
+ const tableRow = tableBody?.querySelector(":only-child");
+
+ expect(ref.current).toBe(tableRow);
+ });
+});
diff --git a/src/components/utrecht/TableRow.tsx b/src/components/utrecht/TableRow.tsx
new file mode 100644
index 00000000..730d0aff
--- /dev/null
+++ b/src/components/utrecht/TableRow.tsx
@@ -0,0 +1,19 @@
+/**
+ * @license EUPL-1.2
+ * Copyright (c) 2022 Robbert Broersma
+ */
+
+import clsx from "clsx";
+import { ForwardedRef, forwardRef, HTMLAttributes, PropsWithChildren } from "react";
+
+type TableRowProps = HTMLAttributes;
+
+export const TableRow = forwardRef(
+ ({ children, className, ...restProps }: PropsWithChildren, ref: ForwardedRef) => (
+
+ {children}
+
+ )
+);
+
+TableRow.displayName = "utrecht-table__row";
diff --git a/styles/globals.css b/styles/globals.css
index 42ca60c6..9b39fdd3 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -4,3 +4,30 @@ body {
margin-inline-end: 0;
margin-inline-start: 0;
}
+
+.example-value--date {
+ color: grey;
+}
+
+.example-value--positive {
+ color: green;
+}
+
+.sr-only {
+ border: 0 !important;
+ clip: rect(1px, 1px, 1px, 1px) !important;
+ -webkit-clip-path: inset(50%) !important;
+ clip-path: inset(50%) !important;
+ height: 1px !important;
+ overflow: hidden !important;
+ margin: -1px !important;
+ padding: 0 !important;
+ position: absolute !important;
+ width: 1px !important;
+ white-space: nowrap !important;
+}
+
+.reverse {
+ display: flex;
+ flex-direction: column-reverse;
+}
| |